diff --git a/kotlin/.buildscript/android-sample-app.gradle b/.buildscript/android-sample-app.gradle similarity index 100% rename from kotlin/.buildscript/android-sample-app.gradle rename to .buildscript/android-sample-app.gradle diff --git a/kotlin/.buildscript/android-ui-tests.gradle b/.buildscript/android-ui-tests.gradle similarity index 100% rename from kotlin/.buildscript/android-ui-tests.gradle rename to .buildscript/android-ui-tests.gradle diff --git a/kotlin/.buildscript/binary-validation.gradle b/.buildscript/binary-validation.gradle similarity index 100% rename from kotlin/.buildscript/binary-validation.gradle rename to .buildscript/binary-validation.gradle diff --git a/.buildscript/build_swift_docs.sh b/.buildscript/build_swift_docs.sh deleted file mode 100755 index c71994765..000000000 --- a/.buildscript/build_swift_docs.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/bin/bash -# -# Copyright 2019 Square Inc. -# -# 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. -# - -# This script uses SourceDocs. -# https://github.com/eneko/SourceDocs -# brew install sourcedocs -# It requires Xcode (minimum 10.2) to run. -# -# Usage: ./build_swift_docs.sh OUTPUT_DIR - -SOURCEDOCS_OUTPUT_DIR="$1" -WORKFLOW_SCHEMES="Workflow WorkflowUI WorkflowTesting" - -if [[ -z "$SOURCEDOCS_OUTPUT_DIR" ]]; then - echo "No output dir specified. Usage: \`build_swift_docs.sh [OUTPUT_DIR]\`" - exit 1 -fi - -set -ex - -# Prepare the Xcode project. -bundle exec pod gen Development.podspec -cd gen/Development - -# Generate the API docs. -for scheme in $WORKFLOW_SCHEMES; do - sourcedocs generate \ - --output-folder "$SOURCEDOCS_OUTPUT_DIR/$scheme" \ - -- \ - -scheme $scheme \ - -workspace Development.xcworkspace -done diff --git a/kotlin/.buildscript/configure-android-defaults.gradle b/.buildscript/configure-android-defaults.gradle similarity index 100% rename from kotlin/.buildscript/configure-android-defaults.gradle rename to .buildscript/configure-android-defaults.gradle diff --git a/kotlin/.buildscript/configure-maven-publish.gradle b/.buildscript/configure-maven-publish.gradle similarity index 100% rename from kotlin/.buildscript/configure-maven-publish.gradle rename to .buildscript/configure-maven-publish.gradle diff --git a/.buildscript/update_changelog.swift b/.buildscript/update_changelog.swift deleted file mode 100755 index 610f0b3bc..000000000 --- a/.buildscript/update_changelog.swift +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/swift - -import Foundation - -let contents = try! String(contentsOfFile: "CHANGELOG.md", encoding: .utf8) - -var lines = contents.components(separatedBy: "\n") - -let workflowVersion = ProcessInfo.processInfo.environment["WORKFLOW_VERSION"]! -let kotlinChangelog = ProcessInfo.processInfo.environment["KOTLIN_CHANGELOG"]! -let swiftChangelog = ProcessInfo.processInfo.environment["SWIFT_CHANGELOG"]! - -let dateFormatter = DateFormatter() -dateFormatter.dateFormat = "YYYY-MM-dd" - -let newChangelog = """ - -## Version \(workflowVersion) - -_\(dateFormatter.string(from: Date()))_ - -### Kotlin - -\(kotlinChangelog) - -### Swift - -\(swiftChangelog) -""" - -lines.insert(newChangelog, at: 2) - -try! lines.joined(separator: "\n").write(toFile: "CHANGELOG.md", atomically: true, encoding: .utf8) diff --git a/kotlin/.editorconfig b/.editorconfig similarity index 100% rename from kotlin/.editorconfig rename to .editorconfig diff --git a/.gen_config.yml b/.gen_config.yml deleted file mode 100644 index d0f149a6a..000000000 --- a/.gen_config.yml +++ /dev/null @@ -1,11 +0,0 @@ -local_sources: - - . - - swift/Samples/SplitScreenContainer - - swift/Samples/BackStackContainer - - swift/Samples/ModalContainer - - swift/Samples/AlertContainer - -platforms: - - ios -podspec_paths: - - Development.podspec diff --git a/.github/workflows/kotlin.yml b/.github/workflows/kotlin.yml index 48c87307a..67924d666 100644 --- a/.github/workflows/kotlin.yml +++ b/.github/workflows/kotlin.yml @@ -4,15 +4,7 @@ on: push: branches: - trunk - paths: - # Rebuild when workflow configs change. - - .github/workflows/kotlin.yml - # Or when kotlin code changes. - - kotlin/** pull_request: - paths: - - .github/workflows/kotlin.yml - - kotlin/** env: GRADLE_HOME: ${{ github.workspace }}/gradle-home @@ -59,13 +51,11 @@ jobs: ## Actual task - name: Assemble with gradle - working-directory: ./kotlin run: ./gradlew assemble --build-cache --no-daemon --stacktrace --gradle-user-home "$GRADLE_HOME" # This should ideally be done in the Check job below, but until gradle caching is fixed we # need to do it after assembling. See https://github.com/square/workflow/issues/1152. - name: Run dokka to validate kdoc - working-directory: ./kotlin run: ./gradlew dokka siteDokka --build-cache --no-daemon --stacktrace --gradle-user-home "$GRADLE_HOME" # Runs all check tasks in parallel. @@ -106,7 +96,6 @@ jobs: ## Actual task - name: Check with Gradle - working-directory: ./kotlin run: ./gradlew ${{ matrix.gradle-task }} --build-cache --no-daemon --stacktrace --gradle-user-home "$GRADLE_HOME" instrumentation-tests: @@ -144,7 +133,6 @@ jobs: - name: Instrumentation Tests uses: reactivecircus/android-emulator-runner@v2 with: - working-directory: ./kotlin api-level: ${{ matrix.api-level }} arch: x86_64 script: ./gradlew connectedCheck --build-cache --no-daemon --stacktrace --gradle-user-home "$GRADLE_HOME" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 5ae37ee49..000000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,164 +0,0 @@ -name: Release Workflow - -on: - repository_dispatch: - types: [release] - -env: - RELEASE_TYPE: ${{ github.event.client_payload.release_type }} - WORKFLOW_VERSION: ${{ github.event.client_payload.workflow_version }} - SWIFT_CHANGELOG: ${{ github.event.client_payload.swift_changelog }} - KOTLIN_CHANGELOG: ${{ github.event.client_payload.kotlin_changelog }} - PUBLISH_SWIFT: ${{ github.event.client_payload.publish_swift }} - PUBLISH_KOTLIN: ${{ github.event.client_payload.publish_kotlin }} - PREFIX_FOR_TEST: ${{ github.event.client_payload.test_prefix }} - -jobs: - bump-trunk: - runs-on: macos-latest - - steps: - - name: Calculate Release Branch - run: | - MAJOR=$(cut -d'.' -f1 <<<'${{ env.WORKFLOW_VERSION }}') - MINOR=$(cut -d'.' -f2 <<<'${{ env.WORKFLOW_VERSION }}') - echo "::set-env name=RELEASE_BRANCH::${{ env.PREFIX_FOR_TEST }}release-v$MAJOR.$MINOR.x" - - - name: Checkout - uses: actions/checkout@v2 - - - name: Checkout Trunk - uses: actions/checkout@v2 - with: - ref: ${{ env.PREFIX_FOR_TEST }}trunk - path: trunk - - - name: Setup Release Branch (major, minor) - if: env.RELEASE_TYPE == 'major' || env.RELEASE_TYPE == 'minor' - run: | - cp -R trunk release - cd release - git checkout -b ${{ env.RELEASE_BRANCH }} - - - name: Setup Release Branch (patch) - if: env.RELEASE_TYPE == 'patch' - uses: actions/checkout@v2 - with: - ref: ${{ env.RELEASE_BRANCH }} - path: release - - - name: Update Changelog - run: | - cd trunk - ../.buildscript/update_changelog.swift - cd ../release - ../.buildscript/update_changelog.swift - - - name: Update Trunk Version (major, minor) - if: env.RELEASE_TYPE == 'major' || env.RELEASE_TYPE == 'minor' - run: | - cd trunk - sed -i '' -e 's/VERSION_NAME=\(.*\)-SNAPSHOT/VERSION_NAME=${{ env.WORKFLOW_VERSION }}-SNAPSHOT/g' kotlin/gradle.properties - ls Workflow*.podspec | xargs sed -i '' -e "s/ s.version\( *=\).*/ s.version\1 '${{ env.WORKFLOW_VERSION }}'/" - - - name: Push changes to trunk - env: - GIT_USERNAME: ${{ github.actor }} - GIT_PASSWORD: ${{ secrets.GITHUB_TOKEN }} - run: | - cd trunk - git add -A . && git commit -m "Releasing ${{ env.WORKFLOW_VERSION }}" && git push -f - - - name: Push Release Branch - run: | - cd release - sed -i '' -e 's/VERSION_NAME=\(.*\)-SNAPSHOT/VERSION_NAME=${{ env.WORKFLOW_VERSION }}/g' kotlin/gradle.properties - sed -i '' -e 's/VERSION_NAME=\(.*\)/VERSION_NAME=${{ env.WORKFLOW_VERSION }}/g' kotlin/gradle.properties - ls Workflow*.podspec | xargs sed -i '' -e "s/ s.version\( *=\).*/ s.version\1 '${{ env.WORKFLOW_VERSION }}'/" - git add -A .; git commit -m "Releasing ${{ env.WORKFLOW_VERSION }}" - git tag ${{ env.PREFIX_FOR_TEST }}v${{ env.WORKFLOW_VERSION }} - git push origin ${{ env.RELEASE_BRANCH }} ${{ env.PREFIX_FOR_TEST }}v${{ env.WORKFLOW_VERSION }} - - - name: Upload Kotlin Artifacts - if: env.PUBLISH_KOTLIN == 'true' - run: | - echo "TODO: Publish Kotlin Artifacts" - - - name: Push to Cocoapods - if: env.PUBLISH_SWIFT == 'true' - run: | - echo "TODO: Push to Cocoapods" - - # Publish Documentation - # Gradle caches (keys must match those defined in kotlin.yml) - # Don't use the gradle wrapper cache, since there's only one job we're downloading the whole wrapper once either way. - - name: Cache gradle artifacts - uses: actions/cache@v1 - with: - path: ~/.gradle/caches - key: gradle-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties') }}-${{ hashFiles('**/*.gradle*') }}-${{ hashFiles('**/buildSrc/**') }}-${{ github.sha }} - restore-keys: | - gradle-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties') }}-${{ hashFiles('**/*.gradle*') }}- - - # Swift caches (keys must match those defined in swift.yml) - - name: Load gem cache - uses: actions/cache@v1 - with: - path: release/.bundle - key: gems-${{ hashFiles('Gemfile.lock') }} - - - name: Set up Swift environment - run: | - # Set global bundle path so it gets used by build_swift_docs.sh running in the nested repo as well. - cd release - bundle config --global path "$(pwd)/.bundle" - bundle check || bundle install - # Don't need to run pod gen, the website script does that itself. - brew install sourcedocs - sudo xcode-select -s /Applications/Xcode_11.4.app - - # Docs dependencies - - name: Set up Python - uses: actions/setup-python@v1 - with: - python-version: 3.6 - - - name: Install Python dependencies - run: | - python -m pip install --upgrade pip - pip install -r requirements.txt - - # This environment variable step should be run after all 3rd-party actions to ensure nothing - # else accidentally overrides any of our special variables. - - name: 'If in test-mode: enable dry run' - if: env.PREFIX_FOR_TEST != '' - run: | - # When PREFIX_FOR_TEST is not empty, we shouldn't actually deploy, just do a dry run to make - # sure all the dependencies are set up correctly. - echo "::set-env name=DRY_RUN::true" - - - name: Debug info - run: | - cd release - echo event_name=${{ github.event_name }} - echo GITHUB_REF=$GITHUB_REF - echo GITHUB_HEAD_REF=$GITHUB_HEAD_REF - echo DRY_RUN=$DRY_RUN - git remote -v - - ## Main steps - - name: Build and deploy website - env: - WORKFLOW_GOOGLE_ANALYTICS_KEY: ${{ secrets.WORKFLOW_GOOGLE_ANALYTICS_KEY }} - GIT_USERNAME: ${{ github.actor }} - GIT_PASSWORD: ${{ secrets.GITHUB_TOKEN }} - run: | - cd release - ./deploy_website.sh ${{ env.PREFIX_FOR_TEST }}v${{ env.WORKFLOW_VERSION }} - - - - name: Create Github Release - run: | - echo "TODO: Create Github Release" - - diff --git a/.github/workflows/swift.yaml b/.github/workflows/swift.yaml deleted file mode 100644 index aa0612f3d..000000000 --- a/.github/workflows/swift.yaml +++ /dev/null @@ -1,118 +0,0 @@ -name: Swift CI - -on: - push: - branches: - - trunk - - '!gh-pages' - paths: - - '*.podspec' - - 'Gemfile*' - - 'Package.swift' - - 'swift/**' - - '.github/workflows/swift.yaml' - - '.buildscript/build_swift_docs.sh' - pull_request: - paths: - - '*.podspec' - - 'Gemfile*' - - 'Package.swift' - - 'swift/**' - - '.github/workflows/swift.yaml' - - '.buildscript/build_swift_docs.sh' - -jobs: - development-apps: - runs-on: macos-latest - - strategy: - matrix: - scheme: - - Development-Unit-WorkflowTests - - Development-Unit-WorkflowUITests - - Development-Unit-SplitScreenTests - - Development-Unit-TicTacToeTests - - steps: - - uses: actions/checkout@v1 - - - name: Cache gems - uses: actions/cache@v1 - with: - path: .bundle - key: gems-${{ hashFiles('Gemfile.lock') }} - - - name: Bundle Install - run: | - bundle check || bundle install --path .bundle - - - name: Pod Install - run: | - bundle exec pod gen Development.podspec - - - name: Switch Xcode - run: sudo xcode-select -s /Applications/Xcode_11.4.app - - - name: Build & Test - run: | - set -o pipefail && xcodebuild -workspace gen/Development/Development.xcworkspace -scheme ${{ matrix.scheme }} -destination platform\=iOS\ Simulator,OS\=13.4,name\=iPad\ Pro\ \(9.7-inch\) build test | xcpretty - - spm: - runs-on: macos-latest - - steps: - - uses: actions/checkout@v1 - - - name: Swift Package Manager - iOS - run: | - xcodebuild -scheme "Workflow-Package" test -destination "name=iPhone 11" - - - name: Swift Package Manager - macOS - run: | - xcodebuild -scheme "Workflow-Package" test - - tutorial: - runs-on: macos-latest - - steps: - - uses: actions/checkout@v1 - - - name: Cache gems - uses: actions/cache@v1 - with: - path: .bundle - key: gems-${{ hashFiles('Gemfile.lock') }} - - - name: Bundle Install - run: | - bundle check || bundle install --path .bundle - - - name: Switch Xcode - run: sudo xcode-select -s /Applications/Xcode_11.4.app - - - name: Tutorial App - run: | - cd swift/Samples/Tutorial - bundle exec pod install - set -o pipefail && xcodebuild -workspace Tutorial.xcworkspace -scheme Tutorial -destination platform\=iOS\ Simulator,OS\=13.4,name\=iPad\ Pro\ \(9.7-inch\) build test | xcpretty - - documentation-lint: - runs-on: macos-latest - - steps: - - uses: actions/checkout@v1 - - - name: Cache gems - uses: actions/cache@v1 - with: - path: .bundle - key: gems-${{ hashFiles('Gemfile.lock') }} - - - name: Bundle Install - run: | - bundle check || bundle install --path .bundle - brew install sourcedocs - - - name: Swiftdocs - run: | - .buildscript/build_swift_docs.sh ${{ runner.temp }}/swiftdocs \ No newline at end of file diff --git a/.github/workflows/validate-documentation.yml b/.github/workflows/validate-documentation.yml index 2d7e5c5e6..a52de08a6 100644 --- a/.github/workflows/validate-documentation.yml +++ b/.github/workflows/validate-documentation.yml @@ -5,30 +5,11 @@ on: paths: # Rebuild when workflow configs change. - .github/workflows/validate-documentation.yml - # Or when documentation code changes. - - 'docs/**' - '**.md' - - mkdocs.yml - lint_docs.sh - .markdownlint.rb jobs: - mkdocs: - name: Build mkdocs to validate mkdocs.yml - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up Python - uses: actions/setup-python@v1 - with: - python-version: 3.6 - - name: Upgrade pip - run: python -m pip install --upgrade pip - - name: Install dependencies - run: pip install -r requirements.txt - - name: Run mkdocs - run: mkdocs build - lint: name: Lint Markdown files runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index 7af158680..b9beab33b 100644 --- a/.gitignore +++ b/.gitignore @@ -30,33 +30,10 @@ out/ .gradle/ build/ local.properties +.gradletasknamecache + # Intellij *.iml .idea/ - -# cocoapods-generate -gen/ - -# Swift Package Manager -.build/ -Package.resolved -.swiftpm/ - -# CocoaPods -Pods/ -gen/ - -# Xcode -xcuserdata/ - -# Sample workspace -SampleApp.xcworkspace - -# Special Mkdocs files -docs/kotlin/api/ -docs/swift/api/ -site/ - -# ios-snapshot-test-case Failure Diffs -FailureDiffs/ +captures/ diff --git a/.hooks/pre-commit b/.hooks/pre-commit deleted file mode 100755 index a363c193f..000000000 --- a/.hooks/pre-commit +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash -git diff --diff-filter=d --staged --name-only | grep -e '\.swift$' | while read line; do - swift run swiftformat "${line}" --quiet; - git add "$line"; -done \ No newline at end of file diff --git a/.swift-version b/.swift-version deleted file mode 100644 index 6e6366051..000000000 --- a/.swift-version +++ /dev/null @@ -1 +0,0 @@ -5.0 \ No newline at end of file diff --git a/.swiftformat b/.swiftformat deleted file mode 100644 index 1f9d04c81..000000000 --- a/.swiftformat +++ /dev/null @@ -1,59 +0,0 @@ ---indent 4 - ---exclude Pods,swift/Tooling,**Dummy.swift - ---wraparguments before-first ---importgrouping testable-bottom - ---enable blankLinesBetweenScopes ---enable consecutiveSpaces ---enable duplicateImports ---enable elseOnSameLine ---enable linebreakAtEndOfFile ---enable redundantParens # https://google.github.io/swift/#parentheses, https://google.github.io/swift/#enum-cases, https://google.github.io/swift/#trailing-closures ---enable semicolons ---enable sortedImports ---enable spaceAroundBraces ---enable spaceAroundBrackets ---enable spaceAroundOperators ---enable spaceInsideBraces ---enable specifiers ---enable trailingSpace # https://google.github.io/swift/#horizontal-whitespace - ---allman false ---binarygrouping none ---closingparen balanced ---commas always ---conflictmarkers reject ---decimalgrouping none ---elseposition same-line ---empty void ---exponentcase lowercase ---exponentgrouping disabled ---fractiongrouping disabled ---fragment false ---hexgrouping none ---hexliteralcase uppercase ---ifdef indent ---indentcase false ---linebreaks lf ---maxwidth none ---nospaceoperators ---nowrapoperators ---octalgrouping none ---operatorfunc spaced ---patternlet hoist ---self init-only ---selfrequired ---semicolons inline ---specifierorder ---tabwidth unspecified ---trailingclosures ---trimwhitespace always ---wrapcollections before-first ---wrapparameters preserve ---xcodeindentation disabled - ---disable unusedArguments - ---header /*\n * Copyright {year} Square Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * \ \ \ \ http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */ \ No newline at end of file diff --git a/Development.podspec b/Development.podspec deleted file mode 100644 index e450fbbdd..000000000 --- a/Development.podspec +++ /dev/null @@ -1,109 +0,0 @@ -Pod::Spec.new do |s| - s.name = 'Development' - s.version = '0.1.0' - s.summary = 'Infrastructure for Workflow-powered UI' - s.homepage = 'https://www.github.com/square/workflow' - s.license = 'Apache License, Version 2.0' - s.author = 'Square' - s.source = { :git => 'https://github.com/square/workflow.git', :tag => "v#{s.version}" } - - s.ios.deployment_target = '11.0' - s.swift_version = '5.0' - s.dependency 'Workflow' - s.dependency 'WorkflowUI' - s.source_files = 'swift/Samples/Dummy.swift' - - s.subspec 'Dummy' do |ss| - end - - s.default_subspecs = 'Dummy' - - dir = Pathname.new(__FILE__).dirname - snapshot_test_env = { - 'IMAGE_DIFF_DIR' => dir.join('swift/FailureDiffs'), - 'FB_REFERENCE_IMAGE_DIR' => dir.join('swift/Samples/SnapshotTests/ReferenceImages'), - } - - s.scheme = { - environment_variables: snapshot_test_env - } - - s.app_spec 'SampleApp' do |app_spec| - app_spec.source_files = 'swift/Samples/SampleApp/Sources/**/*.swift' - app_spec.resources = 'swift/Samples/SampleApp/Resources/**/*.swift' - end - - s.test_spec 'WorkflowTesting' do |test_spec| - test_spec.requires_app_host = true - test_spec.dependency 'WorkflowTesting' - test_spec.source_files = 'swift/WorkflowTesting/Tests/**/*.swift' - end - - # TODO: Disabled because app specs cannot increase the deployment target of the root - # To use, increase the deployment target of this spec to 13.0 or higher - # - # s.app_spec 'SampleSwiftUIApp' do |app_spec| - # app_spec.ios.deployment_target = '13.0' - # app_spec.dependency 'WorkflowSwiftUI' - # app_spec.pod_target_xcconfig = { - # 'IFNFOPLIST_FILE' => '${PODS_ROOT}/../swift/Samples/SampleSwiftUIApp/SampleSwiftUIApp/Configuration/Info.plist' - # } - # app_spec.source_files = 'SampleSwiftUIApp/SampleSwiftUIApp/**/*.swift' - # end - - s.app_spec 'SampleTicTacToe' do |app_spec| - app_spec.source_files = 'swift/Samples/TicTacToe/Sources/**/*.swift' - app_spec.resources = 'swift/Samples/TicTacToe/Resources/**/*' - app_spec.dependency 'BackStackContainer' - app_spec.dependency 'ModalContainer' - app_spec.dependency 'AlertContainer' - end - - s.test_spec 'TicTacToeTests' do |test_spec| - test_spec.dependency 'Development/SampleTicTacToe' - test_spec.dependency 'WorkflowTesting' - test_spec.dependency 'BackStackContainer' - test_spec.dependency 'ModalContainer' - test_spec.dependency 'AlertContainer' - test_spec.requires_app_host = true - test_spec.app_host_name = 'Development/SampleTicTacToe' - test_spec.source_files = 'swift/Samples/TicTacToe/Tests/**/*.swift' - end - - s.app_spec 'SampleSplitScreen' do |app_spec| - app_spec.dependency 'SplitScreenContainer' - app_spec.source_files = 'swift/Samples/SplitScreenContainer/DemoApp/**/*.swift' - - app_spec.scheme = { - environment_variables: snapshot_test_env - } - end - - s.test_spec 'SplitScreenTests' do |test_spec| - test_spec.dependency 'SplitScreenContainer' - test_spec.dependency 'Development/SampleSplitScreen' - test_spec.app_host_name = 'Development/SampleSplitScreen' - test_spec.requires_app_host = true - test_spec.source_files = 'swift/Samples/SplitScreenContainer/SnapshotTests/**/*.swift' - - test_spec.framework = 'XCTest' - - test_spec.dependency 'iOSSnapshotTestCase' - - test_spec.scheme = { - environment_variables: snapshot_test_env - } - end - - s.test_spec 'WorkflowTests' do |test_spec| - test_spec.requires_app_host = true - test_spec.source_files = 'swift/Workflow/Tests/**/*.swift' - test_spec.framework = 'XCTest' - end - - s.test_spec 'WorkflowUITests' do |test_spec| - test_spec.requires_app_host = true - test_spec.source_files = 'swift/WorkflowUI/Tests/**/*.swift' - test_spec.framework = 'XCTest' - end -end diff --git a/Gemfile b/Gemfile deleted file mode 100644 index 0a5bfb590..000000000 --- a/Gemfile +++ /dev/null @@ -1,5 +0,0 @@ -source 'https://rubygems.org' - -gem 'cocoapods' - -gem 'cocoapods-generate' diff --git a/Gemfile.lock b/Gemfile.lock deleted file mode 100644 index 5ae371666..000000000 --- a/Gemfile.lock +++ /dev/null @@ -1,94 +0,0 @@ -GEM - remote: https://rubygems.org/ - specs: - CFPropertyList (3.0.2) - activesupport (4.2.11.1) - i18n (~> 0.7) - minitest (~> 5.1) - thread_safe (~> 0.3, >= 0.3.4) - tzinfo (~> 1.1) - algoliasearch (1.27.1) - httpclient (~> 2.8, >= 2.8.3) - json (>= 1.5.1) - atomos (0.1.3) - claide (1.0.3) - cocoapods (1.9.1) - activesupport (>= 4.0.2, < 5) - claide (>= 1.0.2, < 2.0) - cocoapods-core (= 1.9.1) - cocoapods-deintegrate (>= 1.0.3, < 2.0) - cocoapods-downloader (>= 1.2.2, < 2.0) - cocoapods-plugins (>= 1.0.0, < 2.0) - cocoapods-search (>= 1.0.0, < 2.0) - cocoapods-stats (>= 1.0.0, < 2.0) - cocoapods-trunk (>= 1.4.0, < 2.0) - cocoapods-try (>= 1.1.0, < 2.0) - colored2 (~> 3.1) - escape (~> 0.0.4) - fourflusher (>= 2.3.0, < 3.0) - gh_inspector (~> 1.0) - molinillo (~> 0.6.6) - nap (~> 1.0) - ruby-macho (~> 1.4) - xcodeproj (>= 1.14.0, < 2.0) - cocoapods-core (1.9.1) - activesupport (>= 4.0.2, < 6) - algoliasearch (~> 1.0) - concurrent-ruby (~> 1.1) - fuzzy_match (~> 2.0.4) - nap (~> 1.0) - netrc (~> 0.11) - typhoeus (~> 1.0) - cocoapods-deintegrate (1.0.4) - cocoapods-disable-podfile-validations (0.1.1) - cocoapods-downloader (1.3.0) - cocoapods-generate (2.0.0) - cocoapods-disable-podfile-validations (~> 0.1.1) - cocoapods-plugins (1.0.0) - nap - cocoapods-search (1.0.0) - cocoapods-stats (1.1.0) - cocoapods-trunk (1.4.1) - nap (>= 0.8, < 2.0) - netrc (~> 0.11) - cocoapods-try (1.1.0) - colored2 (3.1.2) - concurrent-ruby (1.1.6) - escape (0.0.4) - ethon (0.12.0) - ffi (>= 1.3.0) - ffi (1.12.2) - fourflusher (2.3.1) - fuzzy_match (2.0.4) - gh_inspector (1.1.3) - httpclient (2.8.3) - i18n (0.9.5) - concurrent-ruby (~> 1.0) - json (2.3.0) - minitest (5.14.0) - molinillo (0.6.6) - nanaimo (0.2.6) - nap (1.1.0) - netrc (0.11.0) - ruby-macho (1.4.0) - thread_safe (0.3.6) - typhoeus (1.3.1) - ethon (>= 0.9.0) - tzinfo (1.2.7) - thread_safe (~> 0.1) - xcodeproj (1.16.0) - CFPropertyList (>= 2.3.3, < 4.0) - atomos (~> 0.1.3) - claide (>= 1.0.2, < 2.0) - colored2 (~> 3.1) - nanaimo (~> 0.2.6) - -PLATFORMS - ruby - -DEPENDENCIES - cocoapods - cocoapods-generate - -BUNDLED WITH - 2.1.4 diff --git a/Package.swift b/Package.swift deleted file mode 100644 index 8c293eff9..000000000 --- a/Package.swift +++ /dev/null @@ -1,49 +0,0 @@ -// swift-tools-version:5.0 -// The swift-tools-version declares the minimum version of Swift required to build this package. - -import PackageDescription - -let package = Package( - name: "Workflow", - platforms: [ - .iOS("10.0"), - .macOS("10.12"), - ], - products: [ - .library( - name: "Workflow", - targets: ["Workflow"] - ), - .library( - name: "WorkflowUI", - targets: ["WorkflowUI"] - ), - ], - dependencies: [ - .package(url: "https://github.com/ReactiveCocoa/ReactiveSwift.git", from: "6.0.0"), - .package(url: "https://github.com/nicklockwood/SwiftFormat", from: "0.44.9"), - ], - targets: [ - .target( - name: "Workflow", - dependencies: ["ReactiveSwift"], - path: "swift/Workflow/Sources" - ), - .testTarget( - name: "WorkflowTests", - dependencies: ["Workflow"], - path: "swift/Workflow/Tests" - ), - .target( - name: "WorkflowUI", - dependencies: ["Workflow"], - path: "swift/WorkflowUI/Sources" - ), - .testTarget( - name: "WorkflowUITests", - dependencies: ["WorkflowUI"], - path: "swift/WorkflowUI/Tests" - ), - ], - swiftLanguageVersions: [.v5] -) diff --git a/README.md b/README.md index 9f8cf5385..82223e66c 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,9 @@ # workflow -[![GitHub license](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg?style=flat)](https://www.apache.org/licenses/LICENSE-2.0) -[![CocoaPods compatible](https://img.shields.io/cocoapods/v/Workflow.svg)](https://cocoapods.org/pods/Workflow) +![Kotlin CI](https://github.com/square/workflow-kotlin/workflows/Kotlin%20CI/badge.svg) [![Maven Central](https://img.shields.io/maven-central/v/com.squareup.workflow/workflow-core-jvm.svg?label=Maven%20Central)](https://search.maven.org/search?q=g:%22com.squareup.workflow%22) +[![GitHub license](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg?style=flat)](https://www.apache.org/licenses/LICENSE-2.0) +[![Kotlinlang slack](https://img.shields.io/static/v1?label=kotlinlang&message=squarelibraries&color=brightgreen&logo=slack)](https://kotlinlang.slack.com/archives/C5HT9AL7Q) A unidirectional data flow library for Kotlin and Swift, emphasizing: @@ -22,45 +23,7 @@ frameborder="0" allowfullscreen> ## Using Workflows in your project -### Swift - -![Swift CI](https://github.com/square/workflow/workflows/Swift%20CI/badge.svg) - -#### Swift Package Manager - -[![SwiftPM compatible](https://img.shields.io/badge/SwiftPM-compatible-orange.svg)](#swift-package-manager) - -If you are developing your own package, be sure that Workflow is included in `dependencies` -in `Package.swift`: - -```swift -dependencies: [ - .package(url: "git@github.com:square/workflow.git", from: "0.21.1") -] -``` - -In Xcode 11+, add Workflow directly as a dependency to your project with -`File` > `Swift Packages` > `Add Package Dependency...`. Provide the git URL when prompted: `git@github.com:square/workflow.git`. - -#### Cocoapods - -[![CocoaPods compatible](https://img.shields.io/cocoapods/v/Workflow.svg)](https://cocoapods.org/pods/Workflow) - -If you use CocoaPods to manage your dependencies, simply add Workflow and WorkflowUI to your -Podfile: - -```ruby -pod 'Workflow' -pod 'WorkflowUI' -``` - -### Kotlin - -![Kotlin CI](https://github.com/square/workflow/workflows/Kotlin%20CI/badge.svg) -[![Maven Central](https://img.shields.io/maven-central/v/com.squareup.workflow/workflow-core-jvm.svg?label=Maven%20Central)](https://search.maven.org/search?q=g:%22com.squareup.workflow%22) -[![Kotlinlang slack](https://img.shields.io/static/v1?label=kotlinlang&message=squarelibraries&color=brightgreen&logo=slack)](https://kotlinlang.slack.com/archives/C5HT9AL7Q) - -#### Maven Artifacts +### Maven Artifacts Artifacts are hosted on Maven Central. If you're using Gradle, ensure `mavenCentral()` appears in your `repositories` block, and then add dependencies on the following artifacts: @@ -97,7 +60,7 @@ your `repositories` block, and then add dependencies on the following artifacts: -#### Lower-level Artifacts +### Lower-level Artifacts Most code shouldn't need to depend on these directly. They should generally only be used to build higher-level integrations with UI frameworks. @@ -118,7 +81,7 @@ higher-level integrations with UI frameworks. -#### Jetpack Compose support +### Jetpack Compose support [Jetpack Compose](https://developer.android.com/jetpack/compose) is the new (under-development, pre-release) UI toolkit for Android. It is comparable to SwiftUI for iOS. The main UI artifacts in @@ -146,9 +109,7 @@ See that repo for usage info and documentation. ### Support & Contact Workflow maintainers hang out in the [#squarelibraries](https://kotlinlang.slack.com/messages/C5HT9AL7Q) -channel on the [Kotlin Slack](https://surveys.jetbrains.com/s3/kotlin-slack-sign-up?_ga=2.93235285.916482233.1570572671-654176432.1527183673) -and the [#square-libraries-wtf](https://androidstudygroup.slack.com/messages/C03NYGB45) channel on -the Android Study Group Slack. +channel on the [Kotlin Slack](https://surveys.jetbrains.com/s3/kotlin-slack-sign-up?_ga=2.93235285.916482233.1570572671-654176432.1527183673). ## Releasing and Deploying diff --git a/RELEASING.md b/RELEASING.md index b6affc29a..21859309c 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -2,14 +2,6 @@ ## Production Releases ---- - -***Before you begin:*** *Please make sure you are set up with -[`pod trunk`](https://guides.cocoapods.org/making/getting-setup-with-trunk.html) and your CocoaPods -account is a contributor to both the Workflow and WorkflowUI pods. If you need to be added as a -contributor, please [open a ticket requesting access](https://github.com/square/workflow/issues/new), -and assign it to @bencochran or @aquageek.* - --- 1. Merge an update of [the change log](CHANGELOG.md) with the changes since the last release. @@ -39,17 +31,9 @@ and assign it to @bencochran or @aquageek.* 1. Close and release the staging repository at https://oss.sonatype.org. -1. Publish to CocoaPods: - ```bash - bundle exec pod trunk push Workflow.podspec - bundle exec pod trunk push WorkflowTesting.podspec - bundle exec pod trunk push WorkflowUI.podspec - ``` - 1. Bump the version - **Kotlin:** Update the `VERSION_NAME` property in `kotlin/gradle.properties` to the new snapshot version, e.g. `VERSION_NAME=0.2.0-SNAPSHOT`. - - **Swift:** Update `s.version` in `*.podspec` to the new version, e.g. `0.2.0`. 1. Commit the new snapshot version: ``` @@ -82,116 +66,7 @@ and assign it to @bencochran or @aquageek.* git push origin trunk ``` -1. Publish the website. See below. - -## Deploying the documentation website - -Official Workflow documentation lives at . The website content -consists of three parts: - -1. Markdown documentation: Lives in the `docs/` folder, and consists of a set of hand-written - Markdown files that document high-level concepts. The static site generator - [mkdocs](https://www.mkdocs.org/) (with [Material](https://squidfunk.github.io/mkdocs-material/) - theming) is used to convert the Markdown to static, styled HTML. -1. Kotlin API reference: Kdoc embedded in Kotlin source files is converted to GitHub-flavored - Markdown by Dokka and then included in the statically-generated website. -1. Swift API reference: Markup comments from Swift files are converted Markdown by - [Sourcedocs](https://github.com/eneko/SourceDocs) and then included in the statically-generated - website. - -**Note: The documentation site is automatically built and deployed whenever a version tag is pushed. -You only need these steps if you want to work on the site locally.** - -### Setting up the site generators - -If you've already done this, you can skip to _Deploying the website to production_ below. - -#### Kotlin: Dokka - -Dokka runs as a Gradle plugin, so you need to be able to build the Kotlin source with Gradle, but -that's it. To generate the docs manually, run: - -```bash -cd kotlin -./gradlew dokka -``` - -#### Swift: Sourcedocs - -Sourcedocs generates a Markdown site from Swift files. You need Ruby, rubygems, -bundler (2.x), Xcode 10.2+, CocoaPods, and of course Sourcedocs itself, to run it. Assuming you've -already got Xcode, Ruby, and rubygems set up, install the rest of the dependencies: - -```bash -gem install bundler cocoapods -brew install sourcedocs -``` - -If that succeeded, you need to generate an Xcode project before running Sourcedocs: - -```bash -cd swift/Samples/SampleApp/ -bundle exec pod install -# If this is your first time running CocoaPods, that will fail and you'll need to run this instead: -#bundle exec pod install --repo-update -``` - -You can manually generate the docs to verify everything is working correctly by running: - -```bash -#cd swift/Samples/SampleApp/ -sourcedocs generate -- -scheme Workflow -workspace SampleApp.xcworkspace -sourcedocs generate -- -scheme WorkflowUI -workspace SampleApp.xcworkspace -sourcedocs generate -- -scheme WorkflowTesting -workspace SampleApp.xcworkspace -``` - -Note that currently sourcedocs only supports Xcode 10, if you run it with Xcode 11 you might see -an error about Catalyst and only empty READMEs will get generated. - -#### mkdocs - -Mkdocs is written in Python, so you'll need Python 3 and pip in order to run it. Assuming those are -set up, run: - -```bash -pip install -r requirements.txt -``` - -Generate the site manually with: - -```bash -mkdocs build -``` - -While you're working on the documentation files, you can run the site locally with: - -```bash -mkdocs serve -``` - -### Deploying the website to production - -**Note: The documentation site is automatically built and deployed by a Github Workflow whenever a -version tag is pushed. You only need these steps if you want to publish the site manually.** - -Before deploying the website for real, you need to export our Google Analytics key in an environment -variable so that it will get added to the HTML. Get the key from one of the project maintainers, -then add the following to your `.bashrc` and re-source it: - -```bash -export WORKFLOW_GOOGLE_ANALYTICS_KEY=UA-__________-1 -``` - -Now you're ready to publish the site! Just choose a tag or SHA to deploy from, and run: - -```bash -./deploy_website.sh TAG_OR_SHA -# For example: -#./deploy_website.sh v0.18.0 -``` - -This will clone the repo to a temporary directory, checkout the right SHA, build Kotlin and Swift -API docs, generate HTML, and push the newly-generated content to the `gh-pages` branch on GitHub. +1. Publish the website. See https://github.com/square/workflow/blob/trunk/RELEASING.md. ### Validating Markdown @@ -213,7 +88,7 @@ Rules can be configured by editing `.markdownlint.rb`. --- -## Kotlin Notes +## Notes ### Development diff --git a/Workflow.podspec b/Workflow.podspec deleted file mode 100644 index 742f9f38d..000000000 --- a/Workflow.podspec +++ /dev/null @@ -1,27 +0,0 @@ -Pod::Spec.new do |s| - s.name = 'Workflow' - s.version = '0.29.0' - s.summary = 'Reactive application architecture' - s.homepage = 'https://www.github.com/square/workflow' - s.license = 'Apache License, Version 2.0' - s.author = 'Square' - s.source = { :git => 'https://github.com/square/workflow.git', :tag => "v#{s.version}" } - - # 1.7 is needed for `swift_versions` support - s.cocoapods_version = '>= 1.7.0' - - s.swift_versions = ['5.0'] - s.ios.deployment_target = '10.0' - s.osx.deployment_target = '10.12' - - s.source_files = 'swift/Workflow/Sources/*.swift' - - s.dependency 'ReactiveSwift', '~> 6.0.0' - - s.test_spec 'Tests' do |test_spec| - test_spec.source_files = 'swift/Workflow/Tests/**/*.swift' - test_spec.framework = 'XCTest' - test_spec.library = 'swiftos' - end - -end diff --git a/WorkflowSwiftUI.podspec b/WorkflowSwiftUI.podspec deleted file mode 100644 index 8136cd0e3..000000000 --- a/WorkflowSwiftUI.podspec +++ /dev/null @@ -1,22 +0,0 @@ -Pod::Spec.new do |s| - s.name = 'WorkflowSwiftUI' - s.version = '0.29.0' - s.summary = 'Infrastructure for Workflow-powered SwiftUI' - s.homepage = 'https://www.github.com/square/workflow' - s.license = 'Apache License, Version 2.0' - s.author = 'Square' - s.source = { :git => 'https://github.com/square/workflow.git', :tag => "v#{s.version}" } - - # 1.7 is needed for `swift_versions` support - s.cocoapods_version = '>= 1.7.0' - - s.swift_versions = ['5.1'] - s.ios.deployment_target = '13.0' - s.osx.deployment_target = '10.15' - - s.source_files = 'swift/WorkflowSwiftUI/Sources/*.swift' - - s.dependency 'Workflow', "#{s.version}" - - end - diff --git a/WorkflowTesting.podspec b/WorkflowTesting.podspec deleted file mode 100644 index a902e2352..000000000 --- a/WorkflowTesting.podspec +++ /dev/null @@ -1,29 +0,0 @@ -Pod::Spec.new do |s| - s.name = 'WorkflowTesting' - s.version = '0.29.0' - s.summary = 'Reactive application architecture' - s.homepage = 'https://www.github.com/square/workflow' - s.license = 'Apache License, Version 2.0' - s.author = 'Square' - s.source = { :git => 'https://github.com/square/workflow.git', :tag => "v#{s.version}" } - - # 1.7 is needed for `swift_versions` support - s.cocoapods_version = '>= 1.7.0' - - s.swift_versions = ['5.0'] - s.ios.deployment_target = '10.0' - s.osx.deployment_target = '10.12' - - s.source_files = 'swift/WorkflowTesting/Sources/*.swift' - - s.dependency 'ReactiveSwift', '~> 6.0.0' - s.dependency 'Workflow', "#{s.version}" - s.framework = 'XCTest' - - s.test_spec 'Tests' do |test_spec| - test_spec.source_files = 'swift/WorkflowTesting/Tests/**/*.swift' - test_spec.framework = 'XCTest' - test_spec.libraries = 'swiftDispatch', 'swiftFoundation', 'swiftos' - end -end - diff --git a/WorkflowUI.podspec b/WorkflowUI.podspec deleted file mode 100644 index 07ab202bb..000000000 --- a/WorkflowUI.podspec +++ /dev/null @@ -1,27 +0,0 @@ -Pod::Spec.new do |s| - s.name = 'WorkflowUI' - s.version = '0.29.0' - s.summary = 'Infrastructure for Workflow-powered UI' - s.homepage = 'https://www.github.com/square/workflow' - s.license = 'Apache License, Version 2.0' - s.author = 'Square' - s.source = { :git => 'https://github.com/square/workflow.git', :tag => "v#{s.version}" } - - # 1.7 is needed for `swift_versions` support - s.cocoapods_version = '>= 1.7.0' - - s.swift_versions = ['5.0'] - s.ios.deployment_target = '10.0' - s.osx.deployment_target = '10.12' - - s.source_files = 'swift/WorkflowUI/Sources/**/*.swift' - - s.dependency 'Workflow', "#{s.version}" - - s.test_spec 'Tests' do |test_spec| - test_spec.source_files = 'swift/WorkflowUI/Tests/**/*.swift' - test_spec.framework = 'XCTest' - test_spec.library = 'swiftos' - end -end - diff --git a/kotlin/build.gradle.kts b/build.gradle.kts similarity index 100% rename from kotlin/build.gradle.kts rename to build.gradle.kts diff --git a/kotlin/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts similarity index 100% rename from kotlin/buildSrc/build.gradle.kts rename to buildSrc/build.gradle.kts diff --git a/kotlin/buildSrc/src/main/java/Dependencies.kt b/buildSrc/src/main/java/Dependencies.kt similarity index 100% rename from kotlin/buildSrc/src/main/java/Dependencies.kt rename to buildSrc/src/main/java/Dependencies.kt diff --git a/deploy_website.sh b/deploy_website.sh deleted file mode 100755 index 2fb45418c..000000000 --- a/deploy_website.sh +++ /dev/null @@ -1,107 +0,0 @@ -#!/bin/bash -# -# Copyright 2019 Square Inc. -# -# 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. -# - -# The website is built using MkDocs with the Material theme. -# https://squidfunk.github.io/mkdocs-material/ -# It requires Python 3 to run. -# Install the packages with the following command: -# pip install -r requirements.txt -# Preview the site as you're editing it with: -# mkdocs serve -# It also uses CocoaPods and Sourcedocs to build the Swift docs. -# See .buildscript/build_swift_docs.sh for setup info. -# -# Usage deploy_website.sh SHA_OR_REF_TO_DEPLOY -# Set the DRY_RUN flag to any non-null value to skip the actual deploy. -# A custom username/password can be used to authenticate to the git repo by setting -# the GIT_USERNAME and GIT_PASSWORD environment variables. - -# Automatically exit the script on error. -set -e - -if [ -z "$WORKFLOW_GOOGLE_ANALYTICS_KEY" ]; then - echo "Must set WORKFLOW_GOOGLE_ANALYTICS_KEY to deploy." >&2 - exit 1 -fi - -REPO="git@github.com:square/workflow.git" -# Accept username/password overrides from environment variables for Github Actions. -if [ -n "$GIT_USERNAME" -a -n "$GIT_PASSWORD" ]; then - echo "Authenticating as $GIT_USERNAME." - GIT_CREDENTIALS="$GIT_USERNAME:$GIT_PASSWORD" - REPO="https://${GIT_CREDENTIALS}@github.com/square/workflow.git" -else - echo "Authenticating as current user." -fi - -DEPLOY_REF=$1 -if [ -z "$DEPLOY_REF" ]; then - echo "Must pass ref to deploy as first argument." >&2 - exit 1 -fi -# Try to cut any extra refs/ prefix off the ref. Needed for Github Actions, which passes -# something like refs/tags/vX.Y.Z, which is not accepted by git clone. -# Note that for pull requests, because Github does a shallow clone, the ref we get won't exist -# and this will fail, but that's ok because in that case the ref is already cloneable. -# In that case, rev-parse will still print the argument to stdout, but we don't care about -# the error message or non-zero exit code. -set +e -DEPLOY_REF=$(git rev-parse --abbrev-ref $DEPLOY_REF 2>/dev/null) -set -e - -DIR=mkdocs-clone -SWIFT_DOCS_SCRIPT="$(pwd)/.buildscript/build_swift_docs.sh" - -# Delete any existing temporary website clone. -echo "Removing ${DIR}…" -rm -rf $DIR - -# Clone the repo into temp folder if we need to deploy a different ref. -# This lets us run the scripts from this working copy even if docs are being built -# for a different ref. -echo "Shallow-cloning ${DEPLOY_REF}…" -git clone --depth 1 --branch $DEPLOY_REF $REPO $DIR - -# Move working directory into temp folder. -pushd $DIR - -# Need to use the absolute path for these. -SWIFT_API_DIR="$(pwd)/docs/swift/api" -echo "SWIFT_API_DIR=$SWIFT_API_DIR" - -# Generate the Kotlin API docs. -echo "Building Kotlin docs…" -( cd kotlin && ./gradlew assemble --build-cache --quiet && ./gradlew siteDokka --build-cache --quiet ) - -# Generate the Swift API docs. -echo "Building Swift docs…" -$SWIFT_DOCS_SCRIPT $SWIFT_API_DIR - -# Push the new files up to GitHub. -if [ -n "$DRY_RUN" ]; then - echo "DRY_RUN enabled, building mkdocs but skipping gh-deploy and push…" - mkdocs build -else - echo "Running mkdocs gh-deploy --force…" - # Build the site and force-push to the gh-pages branch. - mkdocs gh-deploy --force -fi - -# Delete our temp folder. -echo "Deploy finished, cleaning up…" -popd -rm -rf $DIR diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md deleted file mode 100644 index 309fed10b..000000000 --- a/docs/CHANGELOG.md +++ /dev/null @@ -1 +0,0 @@ -redirect: https://github.com/square/workflow/releases \ No newline at end of file diff --git a/docs/CODE_OF_CONDUCT.md b/docs/CODE_OF_CONDUCT.md deleted file mode 120000 index 0400d5746..000000000 --- a/docs/CODE_OF_CONDUCT.md +++ /dev/null @@ -1 +0,0 @@ -../CODE_OF_CONDUCT.md \ No newline at end of file diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md deleted file mode 120000 index 44fcc6343..000000000 --- a/docs/CONTRIBUTING.md +++ /dev/null @@ -1 +0,0 @@ -../CONTRIBUTING.md \ No newline at end of file diff --git a/docs/RELEASING.md b/docs/RELEASING.md deleted file mode 120000 index a4a7357c3..000000000 --- a/docs/RELEASING.md +++ /dev/null @@ -1 +0,0 @@ -../RELEASING.md \ No newline at end of file diff --git a/docs/code-recipes.md b/docs/code-recipes.md deleted file mode 100644 index 0b7015bee..000000000 --- a/docs/code-recipes.md +++ /dev/null @@ -1,3 +0,0 @@ -# Code Receipes - -_Coming soon!_ diff --git a/docs/css/app.css b/docs/css/app.css deleted file mode 100644 index 48136b7ef..000000000 --- a/docs/css/app.css +++ /dev/null @@ -1,48 +0,0 @@ -@font-face { - font-family: cash-market; - src: url("https://cash-f.squarecdn.com/static/fonts/cash-market/v2/CashMarket-Regular.woff2") format("woff2"); - font-weight: 400; - font-style: normal -} - -@font-face { - font-family: cash-market; - src: url("https://cash-f.squarecdn.com/static/fonts/cash-market/v2/CashMarket-Medium.woff2") format("woff2"); - font-weight: 500; - font-style: normal -} - -@font-face { - font-family: cash-market; - src: url("https://cash-f.squarecdn.com/static/fonts/cash-market/v2/CashMarket-Bold.woff2") format("woff2"); - font-weight: 700; - font-style: normal -} - -body, input { - font-family: cash-market,"Helvetica Neue",helvetica,sans-serif; -} - -.md-typeset h1, .md-typeset h2, .md-typeset h3, .md-typeset h4 { - font-family: cash-market,"Helvetica Neue",helvetica,sans-serif; - line-height: normal; - font-weight: bold; - color: #353535; -} - -button.dl { - font-weight: 300; - font-size: 25px; - line-height: 40px; - padding: 3px 10px; - display: inline-block; - border-radius: 6px; - color: #f0f0f0; - margin: 5px 0; - width: auto; -} - -.logo { - text-align: center; - margin-top: 150px; -} diff --git a/docs/development-process.md b/docs/development-process.md deleted file mode 100644 index 15d300e60..000000000 --- a/docs/development-process.md +++ /dev/null @@ -1,3 +0,0 @@ -# Development Process - -_Coming soon!_ diff --git a/docs/faq.md b/docs/faq.md deleted file mode 100644 index d3acbf720..000000000 --- a/docs/faq.md +++ /dev/null @@ -1,51 +0,0 @@ -# Frequently Asked Questions - -## Isn't this basically React/Elm? - -[React](https://reactjs.org/) and [the Elm architecture](https://guide.elm-lang.org/architecture/) -were both strong influences for this library. However both those libraries are written for -JavaScript. Workflows are written in and for both Kotlin and Swift, making use of features of those -languages, and with usability from those languages as a major design goal. There are also a few -architectural differences: - -| | React | Elm | Workflow | -|---|---|---|---| -| **Modularity** | `Component` | TK | `Workflow` is analogous to React's `Component` | -| **State** | Each `Component` has a `state` property that is read directly and updated via a `setState` method. | State is called `Model` in Elm. | `Workflow`s have an associated state type. The state can only be updated when the props change, or with a `WorkflowAction`. | -| **Views** | `Component`s have a `render` method that returns a tree of elements. | Elm applications have a `view` function that returns a tree of elements. | Since workflows are not tied to any particular UI view layer, they can have an arbitrary rendering type. The `render()` method returns this type. | -| **Dependencies** | React allows parent components to pass "props" down to their children. | TK | In Swift, `Workflow`s are often structs that need to be initialized with their dependencies and configuration data from their parent. In Kotlin, they have a separate type parameter (`PropsT`) that is always passed down from the parent. `Workflow` instances can also inject dependencies, and play nicely with dependency injection frameworks. -| **Composability** | TK | TK | TK | -| **Event Handling** | TK | TK | TK | - -## How is this different than MvRx? - -Besides being very Android and Rx specific, MvRx solves view modeling problems only -per screen. Workflow was mainly inspired by the need to manage and compose -navigation in apps with dozens or hundreds of screens. - -## How do I get involved and/or contribute? - -- [Workflow is open source!](https://github.com/square/workflow) -- See our [CONTRIBUTING](https://github.com/square/workflow/blob/trunk/CONTRIBUTING.md) doc to get - started. -- Stay tuned! We're considering hosting a public Slack channel for open source contributors. - -## This seems clever. Can I stick with a traditional development approach? - -Of course! Workflow was designed to make complex application architecture predictable and safe for -large development teams. We're confident that it brings benefits even to smaller projects, but there -is never only one right way to build software. We recommend to [follow good practices and use an -architecture that makes sense for your project](https://www.thoughtworks.com/insights/blog/write-quality-mobile-apps-any-architecture). - -## Why do we need another architecture? - -Architectural patterns with weak access controls and heavy use of shared mutable state make it -incredibly difficult to fully understand the behavior of the code that we are writing. This quickly -devolves into an arms race as the codebase grows: if every feature or component in the codebase -might change anything at any time, bug fixes turn into a really sad game of whack-a-mole. - -We have seen this pattern occur repeatedly in traditional mobile applications using patterns like -MVC. - -Workflow defines strong boundaries and contracts between separate parts of the application to ensure -that our code remains predictable and maintainable as the size and complexity of the codebase grows. diff --git a/docs/images/icon-square.png b/docs/images/icon-square.png deleted file mode 100644 index bdc98d1c2..000000000 Binary files a/docs/images/icon-square.png and /dev/null differ diff --git a/docs/images/workflow_components_diagram.png b/docs/images/workflow_components_diagram.png deleted file mode 100644 index 9f0d8c0e3..000000000 Binary files a/docs/images/workflow_components_diagram.png and /dev/null differ diff --git a/docs/index.md b/docs/index.md deleted file mode 120000 index 32d46ee88..000000000 --- a/docs/index.md +++ /dev/null @@ -1 +0,0 @@ -../README.md \ No newline at end of file diff --git a/docs/tutorial/adding-workflow-to-a-project.md b/docs/tutorial/adding-workflow-to-a-project.md deleted file mode 100644 index 2f064f8de..000000000 --- a/docs/tutorial/adding-workflow-to-a-project.md +++ /dev/null @@ -1,29 +0,0 @@ -# Adding Workflow to a project - -This document will guide you through the process of adding Workflow to an iOS project. - -## Libraries - -You'll need the following four libraries: - -```swift -import Workflow -import WorkflowUI -import ReactiveSwift -``` - -The easiest way to integrate these libraries is via Cocoapods. If you are using Cocoapods, you can -simply add the dependencies to your `.podspec`. - -```ruby -# MySoftware.podspec -Pod::Spec.new do |s| - # ... - - s.dependency 'Workflow' - s.dependency 'WorkflowUI' - s.dependency 'ReactiveSwift' - - # ... -end -``` diff --git a/docs/tutorial/building-a-view-controller-from-screen.md b/docs/tutorial/building-a-view-controller-from-screen.md deleted file mode 100644 index a3d35258e..000000000 --- a/docs/tutorial/building-a-view-controller-from-screen.md +++ /dev/null @@ -1,72 +0,0 @@ -# Building a View Controller from a Screen - -Now that we have a workflow, we need a way to map our screen to an actual view controller. - -## `ScreenViewController` - -The `ScreenViewController` provides a base class that hides the plumbing of updating a view -controller from a view model update. - -```swift -struct DemoScreen: Screen { - let title: String - let onTap: () -> Void - - func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { - return DemoScreenViewController.description(for: self, environment: environment) - } -} - - -class DemoScreenViewController: ScreenViewController { - - private let button: UIButton - - required init(screen: DemoScreen, environment: ViewEnvironment) { - button = UIButton() - super.init(screen: screen, environment: environment) - - update(screen: screen) - } - - override func viewDidLoad() { - super.viewDidLoad() - - button.addTarget(self, action: #selector(buttonPressed(sender:)), for: .touchUpInside) - - view.addSubview(button) - } - - override func viewWillLayoutSubviews() { - super.viewWillLayoutSubviews() - - button.frame = view.bounds - } - - override func screenDidChange(from previousScreen: DemoScreen, previousEnvironment: ViewEnvironment) { - super.screenDidChange(from: previousScreen, previousEnvironment: previousEnvironment) - update(screen: screen) - } - - private func update(screen: DemoScreen) { - button.setTitle(screen.title, for: .normal) - } - - @objc private func buttonPressed(sender: UIButton) { - screen.onTap() - } - -} -``` - -### Lifecycle - -1. When the view controller is first created, it is given the initial screen value. In the example, - we create the button and set the title for it via the `update` method. -1. The view loads as normal, adding the button the hierarchy and setting up the `target:action` for - the button being pressed. -1. The button is tapped. When the callback is called, we call the `onTap` closure passed into the - screen. The workflow will handle this event, update its state, and a new screen will be rendered. -1. The updated screen is passed to the view controller via the - `screenDidChange(from previousScreen: previousEnvironment: previousEnvironment:)` method. Again, - the view controller updates the title of the button based on what was passed in the screen. diff --git a/docs/tutorial/building-a-workflow.md b/docs/tutorial/building-a-workflow.md deleted file mode 100644 index 3b3ccdbb0..000000000 --- a/docs/tutorial/building-a-workflow.md +++ /dev/null @@ -1,288 +0,0 @@ -# Building a Workflow - -## Introduction - -A simple workflow looks something like this: - -```swift -struct DemoWorkflow: Workflow { - - var name: String - - init(name: String) { - self.name = name - } - -} - -extension DemoWorkflow { - - struct State {} - - func makeInitialState() -> State { - return State() - } - - func workflowDidChange(from previousWorkflow: DemoWorkflow, state: inout State) { - - } - - func render(state: State, context: RenderContext) -> String { - return "Hello, \(name)" - } - -} -``` - -A type conforming to `Workflow` represents a single node in the workflow tree. It should contain any -values that must be provided by its parent (who is generally responsible for creating child -workflows). - -Configuration parameters, strings, network services… If your workflow needs access to a value or -object that it cannot create itself, they should be passed into the workflow's initializer. - -Every workflow defines its own `State` type to contain any data that should persist through -subsequent render passes. - -## Render - -Workflows are only useful when they render a value for use by their parent (or, if they are the root -workflow, for display). This type is very commonly a view model, or `Screen`. The -`render(state:context:)` method has a couple of parameters, so we’ll work through them one by one. - -```swift -func render(state: State, context: RenderContext) -> Rendering -``` - -### `state` - -Contains a value of type `State` to provide access to the current state. Any time the state of -workflow changes, `render` is called again to take into account the change in state. - -### `context` - -The render context: - -- provides a way for a workflow to defer to nested (child) workflows to generate some or all of its - rendered output. We’ll walk through that process later on when we cover composition. -- allows a workflow to request the execution of asynchronous tasks (`Worker`s) -- generates event handlers for use in constructing view models. - -In order for us to see the anything in our app, we'll need to return a `Screen` that can be turned -into a view controller: - -```swift - func render(state: State, context: RenderContext) -> DemoScreen { - return DemoScreen(title: "A nice title") - } -``` - -## Actions, or “Things that advance a workflow” - -So far we have only covered workflows that perform simple tasks like generate strings or simple -screens with no actions. If our workflows take on a complicated roles like generating view models, -however, they will inevitably be required to handle events of some kind – some from UI events such -as button taps, others from infrastructure events such as network responses. - -In conventional UIKit code, it is common to deal with each of those event types differently. The -common pattern is to implement a method like `handleButtonTap(sender:)`. Workflows are more strict -about events, however. Workflows require that all events be expressed as "Workflow Actions." - -These actions should be thought of as the entry point to your workflow. If any action of any kind -happens (that your workflow cares about), it should be modeled as an action. - -```swift -struct DemoWorkflow: Workflow { - /// ... -} - -enum Action: WorkflowAction { - - typealias WorkflowType = DemoWorkflow - - case refreshButtonTapped /// UI event - case refreshRequestFinished(RefreshResponse) /// Network event - - func apply(toState state: inout DemoWorkflow.State) -> DemoWorkflow.Output? { - /// ... - } -} -``` - -## The Update Cycle - -Every time a new action is received, it is applied to the current state of the workflow. If your -workflow does more than simply render values, the action's `apply` is the method where the logic lives. - -There are two things that the `apply(toState:)` method is responsible for: - -- Transitioning state -- (Optionally) emitting an output event - -Note that the `render(state:context:)` method is called after every state change, so you can be sure -that any state changes will be reflected. - -Since we have a way of expressing an event from our UI, we can now use the callback on our view -model to send that event back to the workflow: - -```swift -func render(state: State, context: RenderContext) -> DemoScreen { - // Create a sink of our Action type so we can send actions back to the workflow. - let sink = context.makeSink(of: Action.self) - - return DemoScreen( - title: "A nice title", - onTap: { sink.send(Action.refreshButtonTapped) } -} -``` - -## State - -Some workflows do not need state at all – they simply render values based on the values they were -initialized with. But for more complicated workflows, state management is critical. For example, a -multi-screen flow only functions if we are able to define all of the possible steps (model the -state), remember which one we are currently on (persist state), and move to other steps in the -future (transition state). - -To define your workflow's state, simply implement the associatedtype `State` via an enum or struct. - -```swift -struct WelcomeFlowWorkflow: Workflow { - - enum State { - case splashScreen - case loginFlow - case signupFlow - } - - enum Action: WorkflowAction { - case back - /// ... - } - - /// ... -} -``` - -!!! note - Workflows (and their `State`) should always be implemented through value types (structs and - enums) due to the way the framework handles state changes. This means that you can never capture - references to `self`, but the consistent flow of data pays dividends – try this architecture for - a while and we are confident that you will see the benefits. - -## Workers, or "Asynchronous work the workflow needs done" - -A workflow may need to do some amount of asynchronous work (such as a network request, reading from -a sqlite database, etc). Workers provide a declarative interface to units of asynchronous work. - -To do something asynchronously, we define a worker that has an Output type and defines a `run` -method that that returns a Reactive Swift `SignalProducer`. When this worker will be run, the -`SignalProducer` is subscribed to starting the async task. - -```swift -struct RefreshWorker: Worker { - - enum Output { - case success(String) - case error(Error) - } - - func run() -> SignalProducer { - return SignalProducer(value: .success("We did it!")) - .delay(1.0, on: QueueScheduler.main) - } - - func isEquivalent(to otherWorker: RefreshWorker) -> Bool { - return true - } -} -``` - -Because a Worker is a declarative representation of work, it also needs to define an `isEquivalent` -to guarantee that we are not running more than one at the same time. For the simple example above, -it is always considered equivalent as we want only one of this type of worker running at a time. - -In order to start asynchronous work, the workflow requests it in the render method, looking -something like: - -```swift - public func render(state: State, context: RenderContext) -> DemoScreen { - - context.awaitResult(for: RefreshWorker()) { output -> Action in - switch output { - case .success(let result): - return Action.refreshComplete(result) - case .error(let error): - return Action.refreshError(error) - - } - } - } -``` - -When the context is told to await a result from a worker, the context will do the following: - -- Check if there is already a worker running of the same type: - - If there is not, or `isEquivalent` is false, call `run` on the worker and subscribe to the - `SignalProducer` - - If there is already a worker running and isEquivalent is true, continue to wait for it to - produce an output. -- When the SignalProducer from the Worker returns an output, it is mapped to an Action and handled - the same way as any other action. - -## Output Events - -The last role of the update cycle is to emit output events. As workflows form a hierarchy, it is -common for children to send events up the tree. This may happen when a child workflow finishes or -cancels, for example. - -Workflows can define an output type, which may then be returned by Actions. - -## Composition - -Composition is the primary tool that we can use to manage complexity in a growing application. -Workflows should always be kept small enough to be understandable – less than 150 lines is a good -target. By composing together multiple workflows, complex problems can be broken down into -individual pieces that can be quickly understood by other developers (including future you). - -The context provided to the `render(state:context:)` method defines the API through which -composition is made possible. - -### The Render Context - -The useful role of children is ultimately to provide rendered values (typically screen models) via -their `render(state:context:)` implementation. To obtain that value from a child workflow, the -`rendered(with context:key:)` method is invoked on the child workflow. - -When a workflow is rendered with the context, the context will do the following: - -- Check if the child workflow is new or existing: - - If a workflow with the same type was used during the last render pass, the existing child - workflow will be updated with the new workflow. - - Otherwise, a new child workflow node will be initialized. -- The child workflow's `render(state:context:)` method is called. -- The rendered value is returned. - -In practice, this looks something like this: - -```swift -struct ParentWorkflow: Workflow { - - func render(state: State, context: RenderContext) -> String { - let childWorkflow = ChildWorkflow(text: "Hello, World") - return childWorkflow.rendered(with: context) - } - -} - -struct ChildWorkflow: Workflow { - - var text: String - - // ... - - func render(state: State, context: RenderContext) -> String { - return String(text.reversed()) - } -} -``` diff --git a/docs/tutorial/index.md b/docs/tutorial/index.md deleted file mode 100644 index 2fc975468..000000000 --- a/docs/tutorial/index.md +++ /dev/null @@ -1,38 +0,0 @@ -# Swift - -!!! tip - For a comprehensive tutorial with code that you can build and follow along with, see the - [Tutorials](https://github.com/square/workflow/tree/trunk/swift/Samples/Tutorial#tutorial) in - the repo. - - This section will be restructured soon to incorporate that and Kotlin tutorials. - -The Workflow infrastructure is split into several modules. - -## `Workflow` - -The `Workflow` library contains the core types that are used to implement state-driven workflows, -including the `Workflow` protocol and related indrastructure. - -## `WorkflowUI` - -Contains the basic infrastructure required to build a Workflow-based application that uses `UIKit`. - ---- - -Workflow for iOS makes extensive use of [ReactiveSwift](https://github.com/ReactiveCocoa/ReactiveSwift). -If you are new to reactive programming, you may want to familiarize yourself with some of the -basics. Workflow takes care of a lot of the reactive plumbing in a typical application, but you will -have a better time if you understand what the framework is doing. - -* [Core Reactive Primitives](https://github.com/ReactiveCocoa/ReactiveSwift/blob/master/Documentation/ReactivePrimitives.md) -* [Basic Operators](https://github.com/ReactiveCocoa/ReactiveSwift/blob/master/Documentation/BasicOperators.md) -* [How does ReactiveSwift relate to RxSwift?](https://github.com/ReactiveCocoa/ReactiveSwift/blob/master/Documentation/RxComparison.md) - -## Next Steps - -* [Tooling](tooling.md) -* [Adding Workflow to a Project](adding-workflow-to-a-project.md) -* [Building a Workflow](building-a-workflow.md) -* [Building a View Controller from a Screen](building-a-view-controller-from-screen.md) -* [Using a Workflow to Show UI](using-a-workflow-for-ui.md) diff --git a/docs/tutorial/tooling.md b/docs/tutorial/tooling.md deleted file mode 100644 index da648ab1a..000000000 --- a/docs/tutorial/tooling.md +++ /dev/null @@ -1,12 +0,0 @@ -# Tooling - ---- - -## Xcode templates - -Workflow comes with a set of file templates to simplify the process of building features. After -installation, these templates can be found via `File > New > New File...` in Xcode. - -1. Launch terminal and navigate to the Workflow source directory. -1. Run `./Tooling/Templates/install-xcode-templates.sh`. -1. Restart Xcode. \ No newline at end of file diff --git a/docs/tutorial/using-a-workflow-for-ui.md b/docs/tutorial/using-a-workflow-for-ui.md deleted file mode 100644 index ea0b2ef0e..000000000 --- a/docs/tutorial/using-a-workflow-for-ui.md +++ /dev/null @@ -1,52 +0,0 @@ -# Using a workflow to show UI - -## `ContainerViewController` - -In the Workflow architecture, the container acts as the glue between the state-driven world of -Workflows and the UI that is ultimately displayed. On iOS, the container is implemented as -`ContainerViewController`. - -```swift - -/// Drives view controllers from a root Workflow. -public final class ContainerViewController: UIViewController where ScreenType: Screen { - - /// Emits output events from the bound workflow. - public let output: Signal - - public convenience init(workflow: W) where W.Rendering == ScreenType, W.Output == Output -} - -``` - -The initializer argument is the workflow that will drive your application. - -```swift -import UIKit -import Workflow -import WorkflowUI - -@UIApplicationMain -class AppDelegate: UIResponder, UIApplicationDelegate { - - var window: UIWindow? - - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - let window = UIWindow(frame: UIScreen.main.bounds) - - let container = ContainerViewController( - workflow: DemoWorkflow() - ) - - window.rootViewController = container - self.window = window - window.makeKeyAndVisible() - return true - } -} - -``` - -Now, when the `ContainerViewController` is shown, it will start the workflow and `render` will be -called returning the `DemoScreen`. The container will use `viewControllerDescription` to build -a `DemoScreenViewController` and add it to the view hierarchy to display. diff --git a/docs/userguide/comparison.md b/docs/userguide/comparison.md deleted file mode 100644 index 7c0666770..000000000 --- a/docs/userguide/comparison.md +++ /dev/null @@ -1,3 +0,0 @@ -# Comparison with other frameworks - -_Coming soon!_ diff --git a/docs/userguide/concepts.md b/docs/userguide/concepts.md deleted file mode 100644 index c24c8a06d..000000000 --- a/docs/userguide/concepts.md +++ /dev/null @@ -1,108 +0,0 @@ -# Core Concepts - -## Architectural Concepts - -### Unidirectional Data Flow - -There is a wealth of information on the web about [Unidirectional Data Flow](https://www.google.com/search?q=unidirectional+data+flow), -but it very simply means that there is a single path along which data travel _from_ your business -logic to your UI, and events travel _to_ your business logic from your UI, and they always and only -travel in one direction along that path. For Workflow, this also implies that the UI is (almost) -stateless, and that the interesting state for your app is centralized and not duplicated. - -In practice, this makes program flow much easier to reason about because anytime something happens -in an app, it removes the questions of where the state came from that caused it, which components -got which events, and which sequences of cause and effect actually occurred. It makes unit testing -easier because state and events are explicit, and always live in the same place and flow through the -same APIs, so unit tests only need to test state transitions, for the most part. - -### Declarative vs Imperative - -Traditionally, most mobile code is [“imperative”](https://en.wikipedia.org/wiki/Imperative_programming) -– it consists of instructions for how to build and display the UI. These instructions can include -control flow like loops. Imperative code is usually stateful, state is usually sprinkled all over -the place, and tends to care about instances and identity. When reading imperative code, you almost -have to run an interpreter and keep all the pieces of state in your head to figure out what it does. - -Web UI is traditionally [declarative](https://en.wikipedia.org/wiki/Declarative_programming) – it -describes what to render, and some aspects of how to render it (style), but doesn’t say how to -actually draw it. Declarative code is usually easier to read than imperative code. It -describes what it produces, not how to generate it. Declarative code usually cares more about pure -values than instance identities. However, since computers still need actual instructions at some -point, declarative code requires something else, usually imperative, either a compiler or -interpreter, to actually do something with it. - -Workflow code is written in regular Kotlin or Swift, which are both imperative languages, but the -library encourages you to write your logic in a declarative and functional style. The library -manages state and wiring up event handling for you, so the only code you need to write is code that -is actually interesting for your particular problem. - -#### A note about functional programming - -Kotlin and Swift are not strictly functional programming languages, but both have features that -allow you to write [functional](https://en.wikipedia.org/wiki/Functional_programming)-style code. -Functional code discourages side effects and is generally much easier to test than object-oriented -code. Functional and declarative programming go very well together, and Workflow encourages you to -write such code. - -## Core Components - -![workflow component diagram](../images/workflow_components_diagram.png) - -### Workflows - -The Workflows at the left of the diagram contain all state and business logic for the application. -This is where network requests happen, navigation decisions are made, models are saved to or loaded -from disk – if it's not UI, it's in this box. - -For more information, see [Workflow Core Concepts]. - -### View Models - -The primary job of the Workflows is to emit an observable stream of view models representing the -current state of the application's UI. You will sometimes hear these view models referred to as -'screens', which is just another way to refer to a view model that contains the data for an entire -screen in the app. - -For more information, see [Workflow UI Concepts]. - -### Container - -The container is responsible for plumbing together the two separate halves of the application. It -subscribes to the stream of view models that the workflows provide, then implements the logic to -update the live UI whenever a new view model is emitted. - -For more information, see [Workflow UI Concepts]. - -### UI - -This is typically conventional platform-specific UI code. One important note is that UI code should -never attempt to navigate using system components (navigation controller pushes, modal presentation, -etc). In this architecture the workflows are in charge – any navigation that happens outside of the -workflow will be disregarded and stomped on during the next update cycle. - -For more information, see [Workflow UI Concepts]. - -### Events - -In order for the application to actually do anything, the workflow needs to receive events from the -UI. When the user interacts with the application by, for example, tapping a button, the workflow -receives that event – which may trigger a simple state transition, or more complex behavior such as -a network request. - -For more information, see [Workflow Core Concepts]. - ---- - -!!! info Swift vs Kotlin - While the core shape of the libraries is shared by Swift and Kotlin implementations, some of the - naming and types differ slightly to accommodate each language’s particular type system and - naming conventions. Where those differences occur in this document, they are noted in "Swift vs - Kotlin" blurbs. See [Where Swift and Kotlin Libraries Differ](4_where_swift_and_kotlin_libraries_differ.md) - for an overall summary. - - In general, any time a generic type is referred to as `Foo`, in source code the Swift associated - type is called `Foo` and the Kotlin type parameter is called `FooT`. - -[Workflow Core Concepts]: core-workflow.md -[Workflow UI Concepts]: ui-concepts.md \ No newline at end of file diff --git a/docs/userguide/core-patterns.md b/docs/userguide/core-patterns.md deleted file mode 100644 index a9f7f65d1..000000000 --- a/docs/userguide/core-patterns.md +++ /dev/null @@ -1,71 +0,0 @@ -# Workflow Core: Patterns/Variations - -There are a lot associated/generic types in workflow code – that doesn't mean you always need to use -all of them. Here are some common configurations we've seen. - -## Stateless Workflows - -Remember that workflow state is made up of public and private parts. When a workflow's state -consists entirely of public state (i.e. it's initializer arguments in Swift or `PropsT` in Kotlin), -it can ignore all the machinery for private state. In Swift, the`State` type can be `Void`, and in -`Kotlin` it can be `Unit` – such workflows are often referred to as "stateless", since they have no -state of their own. - -## Props-less Workflows - -Some workflows manage all of their state internally, and have no public state (aka props). In Swift, -this just means the workflow implementation has no parameters (although this is rare, see -_Injecting Dependencies_ below). In Kotlin, the `PropsT` type can be `Unit`. `RenderContext` has -convenience overloads of most of its functions to implicitly pass `Unit` for these workflows. - -## Outputless Workflows - -Workflows that only talk to their parent via their `Rendering`, and never emit any output, are -encouraged to indicate that by using the [bottom type](https://en.wikipedia.org/wiki/Bottom_type) as -their `Output` type. In addition to documenting the fact that the workflow will never output, using -the bottom type also lets the compiler enforce it – code that tries to emit outputs will not -compile. In Swift, the `Output` type is specified as [`Never`](https://nshipster.com/never/). In -Kotlin, use [`Nothing`](https://medium.com/@agrawalsuneet/the-nothing-type-kotlin-2e7df43b0111). - -## Composite Workflows - -Composition is a powerful tool for working with Workflows. A workflow can often accomplish a lot -simply by rendering various children. It may just combine the renderings of multiple children, or -use its props to determine which of a set of children to render. Such workflows can often be -stateless. - -## Props values v. Injected Dependencies - -[Dependency injection](https://en.wikipedia.org/wiki/Dependency_injection) is a technique for making -code less coupled and more testable. In short, it's better for classes/structs to accept their -dependencies when they're created instead of hard-coding them. Workflows typically have dependencies -like specific Workers they need to perform some tasks, child workflows to delegate rendering to, or -helpers for things like network requests, formatting and logging. - -### Swift - -A Swift workflow typically receives its dependencies as initializer arguments, just like its input -values, and is normally instantiated anew by its parent in each call to the parent’s render method. -The [factory pattern](https://en.wikipedia.org/wiki/Factory_method_pattern) can be employed to keep -knowledge of children’s implementation details from leaking into their parents. - -### Kotlin - -Kotlin workflows make a more formal distinction between dependencies and props, via the `PropsT` -parameter type on the Kotlin `Workflow` interface. Dependencies (e.g. a network service) are -typically provided as constructor parameters, while props values (e.g. a record locator) are -provided by the parent as an argument to the `RenderContext.renderChild` method. This works -seamlessly with DI libraries like [Dagger](https://dagger.dev/). - -The careful reader will note that this is technically storing "state" in the workflow instance – -something that is generally discouraged. However, since this "state" is never changed, we can make -an exception for this case. If a workflow has properties, they should _only_ be used to store -injected dependencies or dependencies derived from injected ones (e.g. `Worker`s created from -`Observable`s). - -!!! info Swift vs Kotlin - This difference between Swift and Kotlin practices is a side effect of Kotlin’s lack of a - parallel to Swift’s `Self` type. Kotlin has no practical way to provide a method like Swift’s - `Workflow.workflowDidChange`, which accepts a strongly typed reference to the instance from the - previous run of a parent’s `Render` method. Kotlin’s alternative, - `StatefulWorkflow.onPropsChanged`, requires the extra `PropsT` type parameter. diff --git a/docs/userguide/core-worker.md b/docs/userguide/core-worker.md deleted file mode 100644 index 3ebd85a9e..000000000 --- a/docs/userguide/core-worker.md +++ /dev/null @@ -1,73 +0,0 @@ -# Workflow Core: Worker - -## The Role of a Worker - -`Worker` is a protocol (in Swift) and interface (in Kotlin) that defines an asynchronous task that -can be performed by a `Workflow`. `Worker`s only emit outputs, they do not have a `Rendering` type. -They are similar to child workflows with `Void`/`Unit` rendering types. - -A workflow can ask the infrastructure to await the result of a worker by passing that worker to the -`RenderContext.runningWorker` method within a call to the `render` method. A workflow can handle -outputs from a `Worker`. - -## Workers provide a declarative window into the imperative world - -As nice as it is to write declarative code, real apps need to interact with imperative APIs. Workers -allow wrapping imperative APIs so that Workflows can interact with them in a declarative fashion. -Instead of making imperative "start this, do that, now stop" calls, a Workflow can say "I declare -that this task should now be running" and let the infrastructure worry about ensuring the task is -actually started when necessary, continues running if it was already in flight, and torn down when -it's not needed anymore. - -## Workers can perform side effects - -Unlike workflows' `render` method, which can be called many times and must be idempotent, workers -are started and then ran until completion (or cancellation) – independently of how many times the -workflow running them is actually rendered. This means that side effects that should be performed -only once when a workflow enters a particular state, for example, should be placed into a `Worker` -that the workflow runs while in that state. - -## Workers are cold reactive streams - -Workers are effectively simple wrappers around asynchronous streams with explicit equivalence. In -Swift, workers are backed by ReactiveSwift [`SignalProducer`s](http://reactivecocoa.io/reactiveswift/docs/latest/SignalProducer.html#/s:13ReactiveSwift14SignalProducerV). -In Kotlin, they're backed by Kotlin [`Flow`s](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/). -They are also easily derived from [Reactive Streams Publishers](https://www.reactive-streams.org), -including RxJava `Observable`, `Flowable`, or `Single` instances. - -## Worker subscriptions are managed automatically - -While Workers are _backed_ by reactive streams with library-specific subscription APIs, you never -actually subscribe directly to a worker yourself. Instead, a Workflow asks the infrastructure to -run a worker, and the infrastructure will take care of initializing and tearing down the -subscription as appropriate – much like how child workflows' lifetimes are automatically managed by -the runtime. This makes it impossible to accidentally leak a subscription to a worker. - -## Workers manage their _own_ internal state - -Unlike Workflows, which are effectively collections of functions defining state transitions, Workers -represent long-running tasks. For example, Workers commonly execute network requests. The worker's -stream will open a socket and, either blocking on a background thread or asynchronously, read from -that socket and eventually emit data to the workflow that is running it. - -## Workers define their own equivalence - -Since Workers represent ongoing tasks, the infrastructure needs to be able to tell when two workers -represent the same task (so it doesn't perform the task twice), or when a worker has changed between -render passes such that it needs to be torn down and re-started for the new work. - -For these reasons, any time a workflow requests that a worker be run in sequential render passes, it -is asked to compare itself with its last instance and determine if they are equivalent. In Swift, -this is determined by the `Worker` `isEquivalent:to:` method. `Worker`s that conform to `Equatable` -will automatically get an `isEquivalent:to:` method based on the `Equatable` implementation. In -Kotlin, the `Worker` interface defines the `doesSameWorkAs` method which is passed the previous worker. - -!!! faq "Kotlin: Why don't Workers use `equals`?" - Worker equivalence is a key part of the Worker API. The default implementation of `equals`, - which just compares object identity, is almost always incorrect for workers. Defining a separate - method forces implementers to think about how equivalence is defined. - -## Workers are lifecycle-aware - -Workers are aware of when they're started (just like Workflows), but they are also aware of when -they are torn down. This makes them handy for managing resources as well. diff --git a/docs/userguide/core-workflow.md b/docs/userguide/core-workflow.md deleted file mode 100644 index 9d4ea26f7..000000000 --- a/docs/userguide/core-workflow.md +++ /dev/null @@ -1,298 +0,0 @@ -# Workflow Core: Workflow - -## The Role of a Workflow - -`Workflow` is a protocol (in Swift) and interface (in Kotlin) that defines the contract for a single -node in the workflow hierarchy. - -=== "Swift" - ```Swift - public protocol Workflow: AnyWorkflowConvertible { - - associatedtype State - - associatedtype Output = Never - - associatedtype Rendering - - func makeInitialState() -> State - - func workflowDidChange(from previousWorkflow: Self, state: inout State) - - func render(state: State, context: RenderContext) -> Rendering - - } - - ``` - -=== "Kotlin" - ```Kotlin - abstract class StatefulWorkflow : - Workflow { - - abstract fun initialState( - props: PropsT, - initialSnapshot: Snapshot? - ): StateT - - open fun onPropsChanged( - old: PropsT, - new: PropsT, - state: StateT - ): StateT = state - - abstract fun render( - props: PropsT, - state: StateT, - context: RenderContext - ): RenderingT - - abstract fun snapshotState(state: StateT): Snapshot - } - ``` - -??? faq "Swift: What is `AnyWorkflowConvertible`?" - When a protocol has an associated `Self` type, Swift requires the use of a [type-erasing wrapper](https://medium.com/swiftworld/swift-world-type-erasure-5b720bc0318a) - to store references to instances of that protocol. - [`AnyWorkflow`](/workflow/swift/api/Workflow/Structs/AnyWorkflow.html) is such a wrapper for - `Workflow`. [`AnyWorkflowConvertible`](/workflow/swift/api/Workflow/Protocols/AnyWorkflowConvertible.html) - is a protocol with a single method that returns an `AnyWorkflow`. It is useful as a base type - because it allows instances of `Workflow` to be used directly by any code that requires the - type-erased `AnyWorkflow`. - -??? faq "Kotlin: `StatefulWorkflow` vs `Workflow`" - It is a common practice in Kotlin to divide types into two parts: an interface for public API, - and a class for private implementation. The Workflow library defines a [`Workflow`](/workflow/kotlin/api/workflow-core/com.squareup.workflow/-workflow/) - interface, which should be used as the type of properties and parameters by code that needs to - refer to a particular `Workflow` interface. The `Workflow` interface contains a single method, - which simply returns a `StatefulWorkflow` – a `Workflow` can be described as “anything that can - be expressed as a `StatefulWorkflow`.” - - The library also defines two abstract classes which define the contract for workflows and should - be subclassed to implement your workflows: - - - [**`StatefulWorkflow`**](/workflow/kotlin/api/workflow-core/com.squareup.workflow/-stateful-workflow/) - should be subclassed to implement Workflows that have [private state](#private-state). - - [**`StatelessWorkflow`**](/workflow/kotlin/api/workflow-core/com.squareup.workflow/-stateless-workflow/) - should be subclassed to implement Workflows that _don't_ have any private state. See [Stateless Workflows](#stateless-workflows). - -Workflows have several responsibilities: - -## Workflows have state - -Once a Workflow has been started, it always operates in the context of some state. This state is -divided into two parts: private state, which only the Workflow implementation itself knows about, -which is defined by the `State` type, and properties (or "props"), which is passed to the Workflow -from its parent (more on hierarchical workflows below). - -### Private state - -Every Workflow implementation defines a `State` type to maintain any necessary state while the -workflow is running. - -For example, a tic-tac-toe game might have a state like this: - -=== "Swift" - ```Swift - struct State { - - enum Player { - case x - case o - } - - enum Space { - case unfilled - filled(Player) - } - - // 3 rows * 3 columns = 9 spaces - var spaces: [Space] = Array(repeating: .unfilled, count: 9) - var currentTurn: Player = .x - } - ``` - -=== "Kotlin" - ```Kotlin - data class State( - // 3 rows * 3 columns = 9 spaces - val spaces: List = List(9) { Unfilled }, - val currentTurn: Player = X - ) { - - enum class Player { - X, O - } - - sealed class Space { - object Unfilled : Space() - data class Filled(val player: Player) : Space() - } - } - ``` - -When the workflow is first started, it is queried for an initial state value. From that point -forward, the workflow may advance to a new state as the result of events occurring from various -sources (which will be covered below). - -!!! info "Stateless Workflows" - If a workflow does not have any private state, it is often referred to as a - "stateless workflow". A stateless Workflow is simply a Workflow that has a `Void` or `Unit` - `State` type. See more [below](#stateless-workflows). - -### Props - -Every Workflow implementation also defines data that is passed into it. The Workflow is not able to -modify this state itself, but it may change between render passes. This public state is called -`Props`. - -In Swift, the props are simply defined as properties of the struct implementing Workflow itself. In -Kotlin, the `Workflow` interface defines a separate `PropsT` type parameter. (This additional type -parameter is necessary due to Kotlin’s lack of the `Self` type that Swift workflow’s -`workflowDidChange` method relies upon.) - -=== "Swift" - ```Swift - TK - ``` - -=== "Kotlin" - ```Kotlin - data class Props( - val playerXName: String - val playerOName: String - ) - ``` - -## Workflows are advanced by `WorkflowAction`s - -Any time something happens that should advance a workflow – a UI event, a network response, a -child's output event – actions are used to perform the update. For example, a workflow may respond -to UI events by mapping those events into a type conforming to/implementing `WorkflowAction`. These -types implement the logic to advance a workflow by: - -- Advancing to a new state -- (Optionally) emitting an output event up the tree. - -`WorkflowAction`s are typically defined as enums with associated types (Swift) or sealed classes -(Kotlin), and can include data from the event – for example, the ID of the item in the list that was -clicked. - -Side effects such as logging button clicks to an analytics framework are also typically performed in -actions. - -If you're familiar with React/Redux, `WorkflowAction`s are essentially reducers. - -## Workflows can emit output events up the hierarchy to their parent - -When a workflow is advanced by an action, an optional output event can be sent up the workflow -hierarchy. This is the opportunity for a workflow to notify its parent that something has happened -(and the parent's opportunity to respond to that event by dispatching its own action, continuing up -the tree as long as output events are emitted). - -## Workflows produce an external representation of their state via `Rendering` - -Immediately after starting up, or after a state transition occurs, a workflow will have its `render` -method called. This method is responsible for creating and returning a value of type `Rendering`. -You can think of `Rendering` as the "external published state" of the workflow, and the `render` -function as a map of (`Props` + `State` + childrens' `Rendering`s) -> `Rendering`. While a -workflow's internal state may contain more detailed or comprehensive state, the `Rendering` -(external state) is a type that is useful outside of the workflow. Because a workflow’s render -method may be called by infrastructure for a variety of reasons, it’s important to not perform side -effects when rendering — render methods must be idempotent. Event-based side effects should use -Actions and state-based side effects should use Workers. - -When building an interactive application, the `Rendering` type is commonly (but not always) a view -model that will drive the UI layer. - -## Workflows can respond to UI events - -The `RenderContext` that is passed into `render` as the last parameter provides some useful tools to -assist in creating the `Rendering` value. - -If a workflow is producing a view model, it is common to need an event handler to respond to UI -events. The `RenderContext` has API to create an event handler, called a `Sink`, that when called -will advance the workflow by dispatching an action back to the workflow (for more on actions, see -[below](#workflows-are-advanced-by-actions)). - -=== "Swift" - ```Swift - func render(state: State, context: RenderContext) -> DemoScreen { - // Create a sink of our Action type so we can send actions back to the workflow. - let sink = context.makeSink(of: Action.self) - - return DemoScreen( - title: "A nice title", - onTap: { sink.send(Action.refreshButtonTapped) } - } - ``` - -=== "Kotlin" - ```Kotlin - TK - ``` - -## Workflows form a hierarchy (they may have children) - -As they produce a `Rendering` value, it is common for workflows to delegate some portion of that -work to a _child workflow_. This is done via the `RenderContext` that is passed into the `render` -method. In order to delegate to a child, the parent calls `renderChild` on the context, with the -child workflow as the single argument. The infrastructure will spin up the child workflow (including -initializing its initial state) if this is the first time this child has been used, or, if the child -was also used on the previous `render` pass, the existing child will be updated. Either way, -`render` will immediately be called on the child (by the Workflow infrastructure), and the resulting -child's `Rendering` value will be returned to the parent. - -This allows a parent to return complex `Rendering` types (such as a view model representing the -entire UI state of an application) without needing to model all of that complexity within a single -workflow. - -!!! info "Workflow Identity" - The Workflow infrastructure automatically detects the first time and the last subsequent time - you've asked to render a child workflow, and will automatically initialize the child and clean - it up. In both Swift and Kotlin, this is done using the workflow's concrete type. Both languages - use reflection to do this comparison (e.g. in Kotlin, the workflows' `KClass`es are compared). - - It is an error to render workflows of the same type more than once in the same render pass. - Since type is used for workflow identity, the child rendering APIs take an optional string key - to differentiate between multiple child workflows of the same type. - -## Workflows can subscribe to external event sources - -If a workflow needs to respond to some external event source (e.g. push notifications), the workflow -can ask the context to listen to those events from within the `render` method. - -!!! info "Swift vs Kotlin" - In the Swift library, there is a special API for subscribing to hot streams (`Signal` in - ReactiveSwift). The Kotlin library does not have any special API for subscribing to hot streams - (channels), though it does have extension methods to convert [`ReceiveChannel`s](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-receive-channel/), - and RxJava `Flowable`s and `Observables`, to [`Worker`s](#worker). The reason for this - discrepancy is simply that we don't have any uses of channels yet in production, and so we've - decided to keep the API simpler. If we start using channels in the future, it may make sense to - make subscribing to them a first-class API like in Swift. - -## Workflows can perform asynchronous tasks (Workers) - -`Workers` are very similar in concept to child workflows. Unlike child workflows, however, workers -do not have a `Rendering` type; they only exist to perform a single asynchronous task before sending -zero or more output events back up the tree to their parent. - -For more information about workers, see the [Worker](#worker) section below. - -## Workflows can be saved to and restored from a snapshot (Kotlin only) - -On every render pass, each workflow is asked to create a "snapshot" of its state – a lazily-produced -serialization of the workflow's `State` as a binary blob. These `Snapshot`s are aggregated into a -single `Snapshot` for the entire workflow tree and emitted along with the root workflow's -`Rendering`. When the workflow runtime is started, it can be passed an optional `Snapshot` to -restore the tree from. When non-null, the root workflow's snapshot is extracted and passed to the -root workflow's `initialState`. The workflow can choose to either ignore the snapshot or use it to -restore its `State`. On the first render pass, if the root workflow renders any children that were -also being rendered when the snapshot was taken, those children's snapshots are also extracted from -the aggregate and used to initialize their states. - -!!! faq Why don't Swift Workflows support snapshotting? - Snapshotting was built into Kotlin workflows specifically to support Android's app lifecycle, - which requires apps to serialize their current state before being backgrounded so that they can - be restored in case the system needs to kill the hosting process. iOS apps don't have this - requirement, so the Swift library doesn't need to support it. diff --git a/docs/userguide/implementation.md b/docs/userguide/implementation.md deleted file mode 100644 index c0e6e71de..000000000 --- a/docs/userguide/implementation.md +++ /dev/null @@ -1,3 +0,0 @@ -# Implementation Notes - -_Coming soon!_ diff --git a/docs/userguide/motivation.md b/docs/userguide/motivation.md deleted file mode 100644 index 5ae112405..000000000 --- a/docs/userguide/motivation.md +++ /dev/null @@ -1,3 +0,0 @@ -# Motivation & Architectural Concepts - -_Coming soon!_ diff --git a/docs/userguide/testing-concepts.md b/docs/userguide/testing-concepts.md deleted file mode 100644 index adf06e637..000000000 --- a/docs/userguide/testing-concepts.md +++ /dev/null @@ -1,3 +0,0 @@ -# Workflow Testing - -_Coming soon!_ diff --git a/docs/userguide/ui-concepts.md b/docs/userguide/ui-concepts.md deleted file mode 100644 index 03030aecc..000000000 --- a/docs/userguide/ui-concepts.md +++ /dev/null @@ -1,3 +0,0 @@ -# Workflow UI - -_Coming soon!_ diff --git a/kotlin/gradle.properties b/gradle.properties similarity index 100% rename from kotlin/gradle.properties rename to gradle.properties diff --git a/kotlin/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar similarity index 100% rename from kotlin/gradle/wrapper/gradle-wrapper.jar rename to gradle/wrapper/gradle-wrapper.jar diff --git a/kotlin/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from kotlin/gradle/wrapper/gradle-wrapper.properties rename to gradle/wrapper/gradle-wrapper.properties diff --git a/kotlin/gradlew b/gradlew similarity index 100% rename from kotlin/gradlew rename to gradlew diff --git a/kotlin/gradlew.bat b/gradlew.bat similarity index 100% rename from kotlin/gradlew.bat rename to gradlew.bat diff --git a/kotlin/internal-testing-utils/api/internal-testing-utils.api b/internal-testing-utils/api/internal-testing-utils.api similarity index 100% rename from kotlin/internal-testing-utils/api/internal-testing-utils.api rename to internal-testing-utils/api/internal-testing-utils.api diff --git a/kotlin/internal-testing-utils/build.gradle.kts b/internal-testing-utils/build.gradle.kts similarity index 100% rename from kotlin/internal-testing-utils/build.gradle.kts rename to internal-testing-utils/build.gradle.kts diff --git a/kotlin/internal-testing-utils/gradle.properties b/internal-testing-utils/gradle.properties similarity index 100% rename from kotlin/internal-testing-utils/gradle.properties rename to internal-testing-utils/gradle.properties diff --git a/kotlin/internal-testing-utils/src/main/java/com/squareup/workflow/internal/util/UncaughtExceptionGuard.kt b/internal-testing-utils/src/main/java/com/squareup/workflow/internal/util/UncaughtExceptionGuard.kt similarity index 100% rename from kotlin/internal-testing-utils/src/main/java/com/squareup/workflow/internal/util/UncaughtExceptionGuard.kt rename to internal-testing-utils/src/main/java/com/squareup/workflow/internal/util/UncaughtExceptionGuard.kt diff --git a/kotlin/internal-testing-utils/src/test/java/com/squareup/workflow/internal/util/UncaughtExceptionGuardTest.kt b/internal-testing-utils/src/test/java/com/squareup/workflow/internal/util/UncaughtExceptionGuardTest.kt similarity index 100% rename from kotlin/internal-testing-utils/src/test/java/com/squareup/workflow/internal/util/UncaughtExceptionGuardTest.kt rename to internal-testing-utils/src/test/java/com/squareup/workflow/internal/util/UncaughtExceptionGuardTest.kt diff --git a/kotlin/.gitignore b/kotlin/.gitignore deleted file mode 100644 index 524312ef8..000000000 --- a/kotlin/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -captures/ -.gradletasknamecache diff --git a/kotlin/.idea/dictionaries/workflow.xml b/kotlin/.idea/dictionaries/workflow.xml deleted file mode 100644 index f424539ec..000000000 --- a/kotlin/.idea/dictionaries/workflow.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - atomicfu - coroutine - coroutines - flowable - okio - passthrough - squareup - workflows - - - \ No newline at end of file diff --git a/kotlin/.idea/misc.xml b/kotlin/.idea/misc.xml deleted file mode 100644 index 9053c7ab2..000000000 --- a/kotlin/.idea/misc.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/kotlin/legacy/legacy-workflow-core/README.md b/legacy/legacy-workflow-core/README.md similarity index 100% rename from kotlin/legacy/legacy-workflow-core/README.md rename to legacy/legacy-workflow-core/README.md diff --git a/kotlin/legacy/legacy-workflow-core/api/legacy-workflow-core.api b/legacy/legacy-workflow-core/api/legacy-workflow-core.api similarity index 100% rename from kotlin/legacy/legacy-workflow-core/api/legacy-workflow-core.api rename to legacy/legacy-workflow-core/api/legacy-workflow-core.api diff --git a/kotlin/legacy/legacy-workflow-core/build.gradle.kts b/legacy/legacy-workflow-core/build.gradle.kts similarity index 100% rename from kotlin/legacy/legacy-workflow-core/build.gradle.kts rename to legacy/legacy-workflow-core/build.gradle.kts diff --git a/kotlin/legacy/legacy-workflow-core/gradle.properties b/legacy/legacy-workflow-core/gradle.properties similarity index 100% rename from kotlin/legacy/legacy-workflow-core/gradle.properties rename to legacy/legacy-workflow-core/gradle.properties diff --git a/kotlin/legacy/legacy-workflow-core/src/main/java/com/squareup/workflow/legacy/CoroutineWorkflow.kt b/legacy/legacy-workflow-core/src/main/java/com/squareup/workflow/legacy/CoroutineWorkflow.kt similarity index 100% rename from kotlin/legacy/legacy-workflow-core/src/main/java/com/squareup/workflow/legacy/CoroutineWorkflow.kt rename to legacy/legacy-workflow-core/src/main/java/com/squareup/workflow/legacy/CoroutineWorkflow.kt diff --git a/kotlin/legacy/legacy-workflow-core/src/main/java/com/squareup/workflow/legacy/Reaction.kt b/legacy/legacy-workflow-core/src/main/java/com/squareup/workflow/legacy/Reaction.kt similarity index 100% rename from kotlin/legacy/legacy-workflow-core/src/main/java/com/squareup/workflow/legacy/Reaction.kt rename to legacy/legacy-workflow-core/src/main/java/com/squareup/workflow/legacy/Reaction.kt diff --git a/kotlin/legacy/legacy-workflow-core/src/main/java/com/squareup/workflow/legacy/Reactor.kt b/legacy/legacy-workflow-core/src/main/java/com/squareup/workflow/legacy/Reactor.kt similarity index 100% rename from kotlin/legacy/legacy-workflow-core/src/main/java/com/squareup/workflow/legacy/Reactor.kt rename to legacy/legacy-workflow-core/src/main/java/com/squareup/workflow/legacy/Reactor.kt diff --git a/kotlin/legacy/legacy-workflow-core/src/main/java/com/squareup/workflow/legacy/ReactorException.kt b/legacy/legacy-workflow-core/src/main/java/com/squareup/workflow/legacy/ReactorException.kt similarity index 100% rename from kotlin/legacy/legacy-workflow-core/src/main/java/com/squareup/workflow/legacy/ReactorException.kt rename to legacy/legacy-workflow-core/src/main/java/com/squareup/workflow/legacy/ReactorException.kt diff --git a/kotlin/legacy/legacy-workflow-core/src/main/java/com/squareup/workflow/legacy/Renderer.kt b/legacy/legacy-workflow-core/src/main/java/com/squareup/workflow/legacy/Renderer.kt similarity index 100% rename from kotlin/legacy/legacy-workflow-core/src/main/java/com/squareup/workflow/legacy/Renderer.kt rename to legacy/legacy-workflow-core/src/main/java/com/squareup/workflow/legacy/Renderer.kt diff --git a/kotlin/legacy/legacy-workflow-core/src/main/java/com/squareup/workflow/legacy/Worker.kt b/legacy/legacy-workflow-core/src/main/java/com/squareup/workflow/legacy/Worker.kt similarity index 100% rename from kotlin/legacy/legacy-workflow-core/src/main/java/com/squareup/workflow/legacy/Worker.kt rename to legacy/legacy-workflow-core/src/main/java/com/squareup/workflow/legacy/Worker.kt diff --git a/kotlin/legacy/legacy-workflow-core/src/main/java/com/squareup/workflow/legacy/Workflow.kt b/legacy/legacy-workflow-core/src/main/java/com/squareup/workflow/legacy/Workflow.kt similarity index 100% rename from kotlin/legacy/legacy-workflow-core/src/main/java/com/squareup/workflow/legacy/Workflow.kt rename to legacy/legacy-workflow-core/src/main/java/com/squareup/workflow/legacy/Workflow.kt diff --git a/kotlin/legacy/legacy-workflow-core/src/main/java/com/squareup/workflow/legacy/WorkflowInput.kt b/legacy/legacy-workflow-core/src/main/java/com/squareup/workflow/legacy/WorkflowInput.kt similarity index 100% rename from kotlin/legacy/legacy-workflow-core/src/main/java/com/squareup/workflow/legacy/WorkflowInput.kt rename to legacy/legacy-workflow-core/src/main/java/com/squareup/workflow/legacy/WorkflowInput.kt diff --git a/kotlin/legacy/legacy-workflow-core/src/main/java/com/squareup/workflow/legacy/WorkflowOperators.kt b/legacy/legacy-workflow-core/src/main/java/com/squareup/workflow/legacy/WorkflowOperators.kt similarity index 100% rename from kotlin/legacy/legacy-workflow-core/src/main/java/com/squareup/workflow/legacy/WorkflowOperators.kt rename to legacy/legacy-workflow-core/src/main/java/com/squareup/workflow/legacy/WorkflowOperators.kt diff --git a/kotlin/legacy/legacy-workflow-core/src/main/java/com/squareup/workflow/legacy/WorkflowPool.kt b/legacy/legacy-workflow-core/src/main/java/com/squareup/workflow/legacy/WorkflowPool.kt similarity index 100% rename from kotlin/legacy/legacy-workflow-core/src/main/java/com/squareup/workflow/legacy/WorkflowPool.kt rename to legacy/legacy-workflow-core/src/main/java/com/squareup/workflow/legacy/WorkflowPool.kt diff --git a/kotlin/legacy/legacy-workflow-core/src/main/java/com/squareup/workflow/legacy/WorkflowUpdate.kt b/legacy/legacy-workflow-core/src/main/java/com/squareup/workflow/legacy/WorkflowUpdate.kt similarity index 100% rename from kotlin/legacy/legacy-workflow-core/src/main/java/com/squareup/workflow/legacy/WorkflowUpdate.kt rename to legacy/legacy-workflow-core/src/main/java/com/squareup/workflow/legacy/WorkflowUpdate.kt diff --git a/kotlin/legacy/legacy-workflow-core/src/test/java/com/squareup/workflow/legacy/CoroutineWorkflowTest.kt b/legacy/legacy-workflow-core/src/test/java/com/squareup/workflow/legacy/CoroutineWorkflowTest.kt similarity index 100% rename from kotlin/legacy/legacy-workflow-core/src/test/java/com/squareup/workflow/legacy/CoroutineWorkflowTest.kt rename to legacy/legacy-workflow-core/src/test/java/com/squareup/workflow/legacy/CoroutineWorkflowTest.kt diff --git a/kotlin/legacy/legacy-workflow-core/src/test/java/com/squareup/workflow/legacy/ReactorAsWorkflowIntegrationTest.kt b/legacy/legacy-workflow-core/src/test/java/com/squareup/workflow/legacy/ReactorAsWorkflowIntegrationTest.kt similarity index 100% rename from kotlin/legacy/legacy-workflow-core/src/test/java/com/squareup/workflow/legacy/ReactorAsWorkflowIntegrationTest.kt rename to legacy/legacy-workflow-core/src/test/java/com/squareup/workflow/legacy/ReactorAsWorkflowIntegrationTest.kt diff --git a/kotlin/legacy/legacy-workflow-core/src/test/java/com/squareup/workflow/legacy/ReactorIntegrationTest.kt b/legacy/legacy-workflow-core/src/test/java/com/squareup/workflow/legacy/ReactorIntegrationTest.kt similarity index 100% rename from kotlin/legacy/legacy-workflow-core/src/test/java/com/squareup/workflow/legacy/ReactorIntegrationTest.kt rename to legacy/legacy-workflow-core/src/test/java/com/squareup/workflow/legacy/ReactorIntegrationTest.kt diff --git a/kotlin/legacy/legacy-workflow-core/src/test/java/com/squareup/workflow/legacy/WorkerTest.kt b/legacy/legacy-workflow-core/src/test/java/com/squareup/workflow/legacy/WorkerTest.kt similarity index 100% rename from kotlin/legacy/legacy-workflow-core/src/test/java/com/squareup/workflow/legacy/WorkerTest.kt rename to legacy/legacy-workflow-core/src/test/java/com/squareup/workflow/legacy/WorkerTest.kt diff --git a/kotlin/legacy/legacy-workflow-core/src/test/java/com/squareup/workflow/legacy/WorkflowOperatorsTest.kt b/legacy/legacy-workflow-core/src/test/java/com/squareup/workflow/legacy/WorkflowOperatorsTest.kt similarity index 100% rename from kotlin/legacy/legacy-workflow-core/src/test/java/com/squareup/workflow/legacy/WorkflowOperatorsTest.kt rename to legacy/legacy-workflow-core/src/test/java/com/squareup/workflow/legacy/WorkflowOperatorsTest.kt diff --git a/kotlin/legacy/legacy-workflow-core/src/test/java/com/squareup/workflow/legacy/WorkflowPoolTest.kt b/legacy/legacy-workflow-core/src/test/java/com/squareup/workflow/legacy/WorkflowPoolTest.kt similarity index 100% rename from kotlin/legacy/legacy-workflow-core/src/test/java/com/squareup/workflow/legacy/WorkflowPoolTest.kt rename to legacy/legacy-workflow-core/src/test/java/com/squareup/workflow/legacy/WorkflowPoolTest.kt diff --git a/kotlin/legacy/legacy-workflow-rx2/api/legacy-workflow-rx2.api b/legacy/legacy-workflow-rx2/api/legacy-workflow-rx2.api similarity index 100% rename from kotlin/legacy/legacy-workflow-rx2/api/legacy-workflow-rx2.api rename to legacy/legacy-workflow-rx2/api/legacy-workflow-rx2.api diff --git a/kotlin/legacy/legacy-workflow-rx2/build.gradle.kts b/legacy/legacy-workflow-rx2/build.gradle.kts similarity index 100% rename from kotlin/legacy/legacy-workflow-rx2/build.gradle.kts rename to legacy/legacy-workflow-rx2/build.gradle.kts diff --git a/kotlin/legacy/legacy-workflow-rx2/gradle.properties b/legacy/legacy-workflow-rx2/gradle.properties similarity index 100% rename from kotlin/legacy/legacy-workflow-rx2/gradle.properties rename to legacy/legacy-workflow-rx2/gradle.properties diff --git a/kotlin/legacy/legacy-workflow-rx2/src/main/java/com/squareup/workflow/legacy/rx2/EventChannel.kt b/legacy/legacy-workflow-rx2/src/main/java/com/squareup/workflow/legacy/rx2/EventChannel.kt similarity index 100% rename from kotlin/legacy/legacy-workflow-rx2/src/main/java/com/squareup/workflow/legacy/rx2/EventChannel.kt rename to legacy/legacy-workflow-rx2/src/main/java/com/squareup/workflow/legacy/rx2/EventChannel.kt diff --git a/kotlin/legacy/legacy-workflow-rx2/src/main/java/com/squareup/workflow/legacy/rx2/EventSelectBuilder.kt b/legacy/legacy-workflow-rx2/src/main/java/com/squareup/workflow/legacy/rx2/EventSelectBuilder.kt similarity index 100% rename from kotlin/legacy/legacy-workflow-rx2/src/main/java/com/squareup/workflow/legacy/rx2/EventSelectBuilder.kt rename to legacy/legacy-workflow-rx2/src/main/java/com/squareup/workflow/legacy/rx2/EventSelectBuilder.kt diff --git a/kotlin/legacy/legacy-workflow-rx2/src/main/java/com/squareup/workflow/legacy/rx2/Reactor.kt b/legacy/legacy-workflow-rx2/src/main/java/com/squareup/workflow/legacy/rx2/Reactor.kt similarity index 100% rename from kotlin/legacy/legacy-workflow-rx2/src/main/java/com/squareup/workflow/legacy/rx2/Reactor.kt rename to legacy/legacy-workflow-rx2/src/main/java/com/squareup/workflow/legacy/rx2/Reactor.kt diff --git a/kotlin/legacy/legacy-workflow-rx2/src/main/java/com/squareup/workflow/legacy/rx2/Workers.kt b/legacy/legacy-workflow-rx2/src/main/java/com/squareup/workflow/legacy/rx2/Workers.kt similarity index 100% rename from kotlin/legacy/legacy-workflow-rx2/src/main/java/com/squareup/workflow/legacy/rx2/Workers.kt rename to legacy/legacy-workflow-rx2/src/main/java/com/squareup/workflow/legacy/rx2/Workers.kt diff --git a/kotlin/legacy/legacy-workflow-rx2/src/main/java/com/squareup/workflow/legacy/rx2/WorkflowOperators.kt b/legacy/legacy-workflow-rx2/src/main/java/com/squareup/workflow/legacy/rx2/WorkflowOperators.kt similarity index 100% rename from kotlin/legacy/legacy-workflow-rx2/src/main/java/com/squareup/workflow/legacy/rx2/WorkflowOperators.kt rename to legacy/legacy-workflow-rx2/src/main/java/com/squareup/workflow/legacy/rx2/WorkflowOperators.kt diff --git a/kotlin/legacy/legacy-workflow-rx2/src/main/java/com/squareup/workflow/legacy/rx2/Workflows.kt b/legacy/legacy-workflow-rx2/src/main/java/com/squareup/workflow/legacy/rx2/Workflows.kt similarity index 100% rename from kotlin/legacy/legacy-workflow-rx2/src/main/java/com/squareup/workflow/legacy/rx2/Workflows.kt rename to legacy/legacy-workflow-rx2/src/main/java/com/squareup/workflow/legacy/rx2/Workflows.kt diff --git a/kotlin/legacy/legacy-workflow-rx2/src/test/java/com/squareup/workflow/legacy/rx2/CoroutineEventChannelTest.kt b/legacy/legacy-workflow-rx2/src/test/java/com/squareup/workflow/legacy/rx2/CoroutineEventChannelTest.kt similarity index 100% rename from kotlin/legacy/legacy-workflow-rx2/src/test/java/com/squareup/workflow/legacy/rx2/CoroutineEventChannelTest.kt rename to legacy/legacy-workflow-rx2/src/test/java/com/squareup/workflow/legacy/rx2/CoroutineEventChannelTest.kt diff --git a/kotlin/legacy/legacy-workflow-rx2/src/test/java/com/squareup/workflow/legacy/rx2/ReactorAsWorkflowIntegrationTest.kt b/legacy/legacy-workflow-rx2/src/test/java/com/squareup/workflow/legacy/rx2/ReactorAsWorkflowIntegrationTest.kt similarity index 100% rename from kotlin/legacy/legacy-workflow-rx2/src/test/java/com/squareup/workflow/legacy/rx2/ReactorAsWorkflowIntegrationTest.kt rename to legacy/legacy-workflow-rx2/src/test/java/com/squareup/workflow/legacy/rx2/ReactorAsWorkflowIntegrationTest.kt diff --git a/kotlin/legacy/legacy-workflow-rx2/src/test/java/com/squareup/workflow/legacy/rx2/Rx2ReactorIntegrationTest.kt b/legacy/legacy-workflow-rx2/src/test/java/com/squareup/workflow/legacy/rx2/Rx2ReactorIntegrationTest.kt similarity index 100% rename from kotlin/legacy/legacy-workflow-rx2/src/test/java/com/squareup/workflow/legacy/rx2/Rx2ReactorIntegrationTest.kt rename to legacy/legacy-workflow-rx2/src/test/java/com/squareup/workflow/legacy/rx2/Rx2ReactorIntegrationTest.kt diff --git a/kotlin/legacy/legacy-workflow-rx2/src/test/java/com/squareup/workflow/legacy/rx2/Rx2WorkflowPoolIntegrationTest.kt b/legacy/legacy-workflow-rx2/src/test/java/com/squareup/workflow/legacy/rx2/Rx2WorkflowPoolIntegrationTest.kt similarity index 100% rename from kotlin/legacy/legacy-workflow-rx2/src/test/java/com/squareup/workflow/legacy/rx2/Rx2WorkflowPoolIntegrationTest.kt rename to legacy/legacy-workflow-rx2/src/test/java/com/squareup/workflow/legacy/rx2/Rx2WorkflowPoolIntegrationTest.kt diff --git a/kotlin/legacy/legacy-workflow-rx2/src/test/java/com/squareup/workflow/legacy/rx2/RxAssemblyTrackingRule.kt b/legacy/legacy-workflow-rx2/src/test/java/com/squareup/workflow/legacy/rx2/RxAssemblyTrackingRule.kt similarity index 100% rename from kotlin/legacy/legacy-workflow-rx2/src/test/java/com/squareup/workflow/legacy/rx2/RxAssemblyTrackingRule.kt rename to legacy/legacy-workflow-rx2/src/test/java/com/squareup/workflow/legacy/rx2/RxAssemblyTrackingRule.kt diff --git a/kotlin/legacy/legacy-workflow-rx2/src/test/java/com/squareup/workflow/legacy/rx2/WorkerIntegrationTest.kt b/legacy/legacy-workflow-rx2/src/test/java/com/squareup/workflow/legacy/rx2/WorkerIntegrationTest.kt similarity index 100% rename from kotlin/legacy/legacy-workflow-rx2/src/test/java/com/squareup/workflow/legacy/rx2/WorkerIntegrationTest.kt rename to legacy/legacy-workflow-rx2/src/test/java/com/squareup/workflow/legacy/rx2/WorkerIntegrationTest.kt diff --git a/kotlin/legacy/legacy-workflow-rx2/src/test/java/com/squareup/workflow/legacy/rx2/WorkflowOperatorsTest.kt b/legacy/legacy-workflow-rx2/src/test/java/com/squareup/workflow/legacy/rx2/WorkflowOperatorsTest.kt similarity index 100% rename from kotlin/legacy/legacy-workflow-rx2/src/test/java/com/squareup/workflow/legacy/rx2/WorkflowOperatorsTest.kt rename to legacy/legacy-workflow-rx2/src/test/java/com/squareup/workflow/legacy/rx2/WorkflowOperatorsTest.kt diff --git a/kotlin/legacy/legacy-workflow-test/README.md b/legacy/legacy-workflow-test/README.md similarity index 100% rename from kotlin/legacy/legacy-workflow-test/README.md rename to legacy/legacy-workflow-test/README.md diff --git a/kotlin/legacy/legacy-workflow-test/api/legacy-workflow-test.api b/legacy/legacy-workflow-test/api/legacy-workflow-test.api similarity index 100% rename from kotlin/legacy/legacy-workflow-test/api/legacy-workflow-test.api rename to legacy/legacy-workflow-test/api/legacy-workflow-test.api diff --git a/kotlin/legacy/legacy-workflow-test/build.gradle.kts b/legacy/legacy-workflow-test/build.gradle.kts similarity index 100% rename from kotlin/legacy/legacy-workflow-test/build.gradle.kts rename to legacy/legacy-workflow-test/build.gradle.kts diff --git a/kotlin/legacy/legacy-workflow-test/gradle.properties b/legacy/legacy-workflow-test/gradle.properties similarity index 100% rename from kotlin/legacy/legacy-workflow-test/gradle.properties rename to legacy/legacy-workflow-test/gradle.properties diff --git a/kotlin/legacy/legacy-workflow-test/src/main/java/com/squareup/workflow/legacy/test/Assertions.kt b/legacy/legacy-workflow-test/src/main/java/com/squareup/workflow/legacy/test/Assertions.kt similarity index 100% rename from kotlin/legacy/legacy-workflow-test/src/main/java/com/squareup/workflow/legacy/test/Assertions.kt rename to legacy/legacy-workflow-test/src/main/java/com/squareup/workflow/legacy/test/Assertions.kt diff --git a/kotlin/legacy/legacy-workflow-test/src/main/java/com/squareup/workflow/legacy/test/rx2/EventChannels.kt b/legacy/legacy-workflow-test/src/main/java/com/squareup/workflow/legacy/test/rx2/EventChannels.kt similarity index 100% rename from kotlin/legacy/legacy-workflow-test/src/main/java/com/squareup/workflow/legacy/test/rx2/EventChannels.kt rename to legacy/legacy-workflow-test/src/main/java/com/squareup/workflow/legacy/test/rx2/EventChannels.kt diff --git a/lint_docs.sh b/lint_docs.sh index 2a090ee99..544856e4d 100755 --- a/lint_docs.sh +++ b/lint_docs.sh @@ -23,11 +23,11 @@ set -ex STYLE=.markdownlint.rb -DIR=docs/ # CHANGELOG is an mkdocs redirect pointer, not valid markdown. -find $DIR \ +find . \ -name '*.md' \ -not -name 'CHANGELOG.md' \ + -not -path './.github/*' \ | xargs mdl --style $STYLE --ignore-front-matter \ && echo "Success." diff --git a/mkdocs.yml b/mkdocs.yml deleted file mode 100644 index 4e3867dd0..000000000 --- a/mkdocs.yml +++ /dev/null @@ -1,123 +0,0 @@ -# -# Copyright 2019 Square Inc. -# -# 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. -# - -site_name: Workflow -repo_name: Workflow -repo_url: https://github.com/square/workflow -site_description: "A library for making composable state machines, and UIs driven by those state machines." -site_author: Square, Inc. -site_url: https://square.github.io/workflow/ -remote_branch: gh-pages - -copyright: 'Copyright © 2019 Square, Inc.' - -theme: - name: 'material' - logo: images/icon-square.png - favicon: images/icon-square.png - icon: - repo: fontawesome/brands/github - palette: - primary: 'red' - accent: 'pink' - features: - - tabs - - instant - -extra_css: - - 'css/app.css' - -markdown_extensions: - - admonition - - smarty - - codehilite: - guess_lang: false - linenums: True - - footnotes - - meta - - toc: - permalink: true - - pymdownx.betterem: - smart_enable: all - - pymdownx.caret - - pymdownx.details - - pymdownx.inlinehilite - - pymdownx.magiclink - - pymdownx.smartsymbols - - pymdownx.superfences - - pymdownx.tabbed - - tables - -plugins: - - search - - redirects: - redirect_maps: - # Redirect some of the most-visited pages from their old locations in case there are links - # to these pages somewhere. - 'kotlin/api/workflow-core/com.squareup.workflow/index.md': 'kotlin/api/workflow/com.squareup.workflow/index.md' - 'kotlin/api/workflow-core/com.squareup.workflow/-worker/index.md': 'kotlin/api/workflow/com.squareup.workflow/-worker/index.md' - 'kotlin/api/workflow-testing/com.squareup.workflow.testing/index.md': 'kotlin/api/workflow/com.squareup.workflow.testing/index.md' - 'kotlin/api/workflow-testing/com.squareup.workflow.testing/-render-tester/index.md': 'kotlin/api/workflow/com.squareup.workflow.testing/-render-tester/index.md' - 'kotlin/api/workflow-ui-android/com.squareup.workflow.ui/index.md': 'kotlin/api/workflow/com.squareup.workflow.ui/index.md' - -extra: - # type is the name of the FontAwesome icon without the fa- prefix. - social: - - icon: fontawesome/brands/github-alt - link: https://github.com/square - - icon: fontawesome/brands/twitter - link: https://twitter.com/squareeng - - icon: fontawesome/brands/linkedin - link: https://www.linkedin.com/company/joinsquare/ - -nav: - - 'Overview': index.md - - 'User Guide': - - 'Core Concepts': 'userguide/concepts.md' - - 'Workflow Core': - - 'Workflow': 'userguide/core-workflow.md' - - 'Worker': 'userguide/core-worker.md' - - 'Patterns': 'userguide/core-patterns.md' - - 'Workflow UI': 'userguide/ui-concepts.md' - - 'Workflow Testing': 'userguide/testing-concepts.md' - - 'Motivation & Architectural Concepts': 'userguide/motivation.md' - - 'Comparison with other frameworks': 'userguide/comparison.md' - - 'Under the Hood: Implementation Notes': 'userguide/implementation.md' - - 'Tutorials & Samples': - - 'Tutorial': - - 'Overview': 'tutorial/index.md' - - 'Adding Workflow to a project': 'tutorial/adding-workflow-to-a-project.md' - - 'Tooling': 'tutorial/tooling.md' - - 'Building a Workflow': 'tutorial/building-a-workflow.md' - - 'Building a View Controller from a Screen': 'tutorial/building-a-view-controller-from-screen.md' - - 'Using a Workflow to Show UI': 'tutorial/using-a-workflow-for-ui.md' - - 'Code Recipes': 'code-recipes.md' - - 'Development Process': 'development-process.md' - - 'API Reference': - - 'Kotlin': 'kotlin/api/workflow/index.md' - - 'Swift API': - - 'Workflow ': 'swift/api/Workflow/README.md' - - 'WorkflowUI ': 'swift/api/WorkflowUI/README.md' - - 'WorkflowTesting ': 'swift/api/WorkflowTesting/README.md' - - 'FAQ': faq.md - - 'Change Log ⏏': CHANGELOG.md - - 'Contributing': CONTRIBUTING.md - - 'Code of Conduct': CODE_OF_CONDUCT.md - -# Google Analytics. Add export WORKFLOW_GOOGLE_ANALYTICS_KEY="UA-XXXXXXXXX-X" to your ~/.bashrc -google_analytics: - - !!python/object/apply:os.getenv ["WORKFLOW_GOOGLE_ANALYTICS_KEY"] - - auto diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index c788ac98b..000000000 --- a/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -mkdocs==1.1.2 -mkdocs-material==5.2.2 -mkdocs-redirects==1.0.0 diff --git a/kotlin/samples/containers/android/build.gradle.kts b/samples/containers/android/build.gradle.kts similarity index 100% rename from kotlin/samples/containers/android/build.gradle.kts rename to samples/containers/android/build.gradle.kts diff --git a/kotlin/samples/containers/android/src/main/AndroidManifest.xml b/samples/containers/android/src/main/AndroidManifest.xml similarity index 100% rename from kotlin/samples/containers/android/src/main/AndroidManifest.xml rename to samples/containers/android/src/main/AndroidManifest.xml diff --git a/kotlin/samples/containers/android/src/main/java/com/squareup/sample/container/BackButtonScreen.kt b/samples/containers/android/src/main/java/com/squareup/sample/container/BackButtonScreen.kt similarity index 100% rename from kotlin/samples/containers/android/src/main/java/com/squareup/sample/container/BackButtonScreen.kt rename to samples/containers/android/src/main/java/com/squareup/sample/container/BackButtonScreen.kt diff --git a/kotlin/samples/containers/android/src/main/java/com/squareup/sample/container/SampleContainers.kt b/samples/containers/android/src/main/java/com/squareup/sample/container/SampleContainers.kt similarity index 100% rename from kotlin/samples/containers/android/src/main/java/com/squareup/sample/container/SampleContainers.kt rename to samples/containers/android/src/main/java/com/squareup/sample/container/SampleContainers.kt diff --git a/kotlin/samples/containers/android/src/main/java/com/squareup/sample/container/overviewdetail/OverviewDetailConfig.kt b/samples/containers/android/src/main/java/com/squareup/sample/container/overviewdetail/OverviewDetailConfig.kt similarity index 100% rename from kotlin/samples/containers/android/src/main/java/com/squareup/sample/container/overviewdetail/OverviewDetailConfig.kt rename to samples/containers/android/src/main/java/com/squareup/sample/container/overviewdetail/OverviewDetailConfig.kt diff --git a/kotlin/samples/containers/android/src/main/java/com/squareup/sample/container/overviewdetail/OverviewDetailContainer.kt b/samples/containers/android/src/main/java/com/squareup/sample/container/overviewdetail/OverviewDetailContainer.kt similarity index 100% rename from kotlin/samples/containers/android/src/main/java/com/squareup/sample/container/overviewdetail/OverviewDetailContainer.kt rename to samples/containers/android/src/main/java/com/squareup/sample/container/overviewdetail/OverviewDetailContainer.kt diff --git a/kotlin/samples/containers/android/src/main/java/com/squareup/sample/container/panel/Contexts.kt b/samples/containers/android/src/main/java/com/squareup/sample/container/panel/Contexts.kt similarity index 100% rename from kotlin/samples/containers/android/src/main/java/com/squareup/sample/container/panel/Contexts.kt rename to samples/containers/android/src/main/java/com/squareup/sample/container/panel/Contexts.kt diff --git a/kotlin/samples/containers/android/src/main/java/com/squareup/sample/container/panel/PanelContainer.kt b/samples/containers/android/src/main/java/com/squareup/sample/container/panel/PanelContainer.kt similarity index 100% rename from kotlin/samples/containers/android/src/main/java/com/squareup/sample/container/panel/PanelContainer.kt rename to samples/containers/android/src/main/java/com/squareup/sample/container/panel/PanelContainer.kt diff --git a/kotlin/samples/containers/android/src/main/java/com/squareup/sample/container/panel/ScrimContainer.kt b/samples/containers/android/src/main/java/com/squareup/sample/container/panel/ScrimContainer.kt similarity index 100% rename from kotlin/samples/containers/android/src/main/java/com/squareup/sample/container/panel/ScrimContainer.kt rename to samples/containers/android/src/main/java/com/squareup/sample/container/panel/ScrimContainer.kt diff --git a/kotlin/samples/containers/android/src/main/res/anim-sw600dp/panel_enter.xml b/samples/containers/android/src/main/res/anim-sw600dp/panel_enter.xml similarity index 100% rename from kotlin/samples/containers/android/src/main/res/anim-sw600dp/panel_enter.xml rename to samples/containers/android/src/main/res/anim-sw600dp/panel_enter.xml diff --git a/kotlin/samples/containers/android/src/main/res/anim-sw600dp/panel_exit.xml b/samples/containers/android/src/main/res/anim-sw600dp/panel_exit.xml similarity index 100% rename from kotlin/samples/containers/android/src/main/res/anim-sw600dp/panel_exit.xml rename to samples/containers/android/src/main/res/anim-sw600dp/panel_exit.xml diff --git a/kotlin/samples/containers/android/src/main/res/anim/panel_enter.xml b/samples/containers/android/src/main/res/anim/panel_enter.xml similarity index 100% rename from kotlin/samples/containers/android/src/main/res/anim/panel_enter.xml rename to samples/containers/android/src/main/res/anim/panel_enter.xml diff --git a/kotlin/samples/containers/android/src/main/res/anim/panel_exit.xml b/samples/containers/android/src/main/res/anim/panel_exit.xml similarity index 100% rename from kotlin/samples/containers/android/src/main/res/anim/panel_exit.xml rename to samples/containers/android/src/main/res/anim/panel_exit.xml diff --git a/kotlin/samples/containers/android/src/main/res/layout-ldrtl/overview_detail_split.xml b/samples/containers/android/src/main/res/layout-ldrtl/overview_detail_split.xml similarity index 100% rename from kotlin/samples/containers/android/src/main/res/layout-ldrtl/overview_detail_split.xml rename to samples/containers/android/src/main/res/layout-ldrtl/overview_detail_split.xml diff --git a/kotlin/samples/containers/android/src/main/res/layout/overview_detail_single.xml b/samples/containers/android/src/main/res/layout/overview_detail_single.xml similarity index 100% rename from kotlin/samples/containers/android/src/main/res/layout/overview_detail_single.xml rename to samples/containers/android/src/main/res/layout/overview_detail_single.xml diff --git a/kotlin/samples/containers/android/src/main/res/layout/overview_detail_split.xml b/samples/containers/android/src/main/res/layout/overview_detail_split.xml similarity index 100% rename from kotlin/samples/containers/android/src/main/res/layout/overview_detail_split.xml rename to samples/containers/android/src/main/res/layout/overview_detail_split.xml diff --git a/kotlin/samples/containers/android/src/main/res/values-land/bools.xml b/samples/containers/android/src/main/res/values-land/bools.xml similarity index 100% rename from kotlin/samples/containers/android/src/main/res/values-land/bools.xml rename to samples/containers/android/src/main/res/values-land/bools.xml diff --git a/kotlin/samples/containers/android/src/main/res/values-land/layout.xml b/samples/containers/android/src/main/res/values-land/layout.xml similarity index 100% rename from kotlin/samples/containers/android/src/main/res/values-land/layout.xml rename to samples/containers/android/src/main/res/values-land/layout.xml diff --git a/kotlin/samples/containers/android/src/main/res/values-sw600dp/bools.xml b/samples/containers/android/src/main/res/values-sw600dp/bools.xml similarity index 100% rename from kotlin/samples/containers/android/src/main/res/values-sw600dp/bools.xml rename to samples/containers/android/src/main/res/values-sw600dp/bools.xml diff --git a/kotlin/samples/containers/android/src/main/res/values-sw600dp/layout.xml b/samples/containers/android/src/main/res/values-sw600dp/layout.xml similarity index 100% rename from kotlin/samples/containers/android/src/main/res/values-sw600dp/layout.xml rename to samples/containers/android/src/main/res/values-sw600dp/layout.xml diff --git a/kotlin/samples/containers/android/src/main/res/values/bools.xml b/samples/containers/android/src/main/res/values/bools.xml similarity index 100% rename from kotlin/samples/containers/android/src/main/res/values/bools.xml rename to samples/containers/android/src/main/res/values/bools.xml diff --git a/kotlin/samples/containers/android/src/main/res/values/colors.xml b/samples/containers/android/src/main/res/values/colors.xml similarity index 100% rename from kotlin/samples/containers/android/src/main/res/values/colors.xml rename to samples/containers/android/src/main/res/values/colors.xml diff --git a/kotlin/samples/containers/android/src/main/res/values/ids.xml b/samples/containers/android/src/main/res/values/ids.xml similarity index 100% rename from kotlin/samples/containers/android/src/main/res/values/ids.xml rename to samples/containers/android/src/main/res/values/ids.xml diff --git a/kotlin/samples/containers/android/src/main/res/values/layout.xml b/samples/containers/android/src/main/res/values/layout.xml similarity index 100% rename from kotlin/samples/containers/android/src/main/res/values/layout.xml rename to samples/containers/android/src/main/res/values/layout.xml diff --git a/kotlin/samples/containers/android/src/main/res/values/styles.xml b/samples/containers/android/src/main/res/values/styles.xml similarity index 100% rename from kotlin/samples/containers/android/src/main/res/values/styles.xml rename to samples/containers/android/src/main/res/values/styles.xml diff --git a/kotlin/samples/containers/app-poetry/build.gradle.kts b/samples/containers/app-poetry/build.gradle.kts similarity index 100% rename from kotlin/samples/containers/app-poetry/build.gradle.kts rename to samples/containers/app-poetry/build.gradle.kts diff --git a/kotlin/samples/containers/app-poetry/src/androidTest/java/com/squareup/sample/poetryapp/PoetryAppTest.kt b/samples/containers/app-poetry/src/androidTest/java/com/squareup/sample/poetryapp/PoetryAppTest.kt similarity index 100% rename from kotlin/samples/containers/app-poetry/src/androidTest/java/com/squareup/sample/poetryapp/PoetryAppTest.kt rename to samples/containers/app-poetry/src/androidTest/java/com/squareup/sample/poetryapp/PoetryAppTest.kt diff --git a/kotlin/samples/containers/app-poetry/src/main/AndroidManifest.xml b/samples/containers/app-poetry/src/main/AndroidManifest.xml similarity index 100% rename from kotlin/samples/containers/app-poetry/src/main/AndroidManifest.xml rename to samples/containers/app-poetry/src/main/AndroidManifest.xml diff --git a/kotlin/samples/containers/app-poetry/src/main/java/com/squareup/sample/poetryapp/PoemListLayoutRunner.kt b/samples/containers/app-poetry/src/main/java/com/squareup/sample/poetryapp/PoemListLayoutRunner.kt similarity index 100% rename from kotlin/samples/containers/app-poetry/src/main/java/com/squareup/sample/poetryapp/PoemListLayoutRunner.kt rename to samples/containers/app-poetry/src/main/java/com/squareup/sample/poetryapp/PoemListLayoutRunner.kt diff --git a/kotlin/samples/containers/app-poetry/src/main/java/com/squareup/sample/poetryapp/PoemListWorkflow.kt b/samples/containers/app-poetry/src/main/java/com/squareup/sample/poetryapp/PoemListWorkflow.kt similarity index 100% rename from kotlin/samples/containers/app-poetry/src/main/java/com/squareup/sample/poetryapp/PoemListWorkflow.kt rename to samples/containers/app-poetry/src/main/java/com/squareup/sample/poetryapp/PoemListWorkflow.kt diff --git a/kotlin/samples/containers/app-poetry/src/main/java/com/squareup/sample/poetryapp/PoemsBrowserWorkflow.kt b/samples/containers/app-poetry/src/main/java/com/squareup/sample/poetryapp/PoemsBrowserWorkflow.kt similarity index 100% rename from kotlin/samples/containers/app-poetry/src/main/java/com/squareup/sample/poetryapp/PoemsBrowserWorkflow.kt rename to samples/containers/app-poetry/src/main/java/com/squareup/sample/poetryapp/PoemsBrowserWorkflow.kt diff --git a/kotlin/samples/containers/app-poetry/src/main/java/com/squareup/sample/poetryapp/PoetryActivity.kt b/samples/containers/app-poetry/src/main/java/com/squareup/sample/poetryapp/PoetryActivity.kt similarity index 100% rename from kotlin/samples/containers/app-poetry/src/main/java/com/squareup/sample/poetryapp/PoetryActivity.kt rename to samples/containers/app-poetry/src/main/java/com/squareup/sample/poetryapp/PoetryActivity.kt diff --git a/kotlin/samples/containers/app-poetry/src/main/res/values/strings.xml b/samples/containers/app-poetry/src/main/res/values/strings.xml similarity index 100% rename from kotlin/samples/containers/app-poetry/src/main/res/values/strings.xml rename to samples/containers/app-poetry/src/main/res/values/strings.xml diff --git a/kotlin/samples/containers/app-raven/build.gradle.kts b/samples/containers/app-raven/build.gradle.kts similarity index 100% rename from kotlin/samples/containers/app-raven/build.gradle.kts rename to samples/containers/app-raven/build.gradle.kts diff --git a/kotlin/samples/containers/app-raven/src/androidTest/java/com/squareup/sample/ravenapp/RavenAppTest.kt b/samples/containers/app-raven/src/androidTest/java/com/squareup/sample/ravenapp/RavenAppTest.kt similarity index 100% rename from kotlin/samples/containers/app-raven/src/androidTest/java/com/squareup/sample/ravenapp/RavenAppTest.kt rename to samples/containers/app-raven/src/androidTest/java/com/squareup/sample/ravenapp/RavenAppTest.kt diff --git a/kotlin/samples/containers/app-raven/src/main/AndroidManifest.xml b/samples/containers/app-raven/src/main/AndroidManifest.xml similarity index 100% rename from kotlin/samples/containers/app-raven/src/main/AndroidManifest.xml rename to samples/containers/app-raven/src/main/AndroidManifest.xml diff --git a/kotlin/samples/containers/app-raven/src/main/java/com/squareup/sample/ravenapp/RavenActivity.kt b/samples/containers/app-raven/src/main/java/com/squareup/sample/ravenapp/RavenActivity.kt similarity index 100% rename from kotlin/samples/containers/app-raven/src/main/java/com/squareup/sample/ravenapp/RavenActivity.kt rename to samples/containers/app-raven/src/main/java/com/squareup/sample/ravenapp/RavenActivity.kt diff --git a/kotlin/samples/containers/app-raven/src/main/res/values/strings.xml b/samples/containers/app-raven/src/main/res/values/strings.xml similarity index 100% rename from kotlin/samples/containers/app-raven/src/main/res/values/strings.xml rename to samples/containers/app-raven/src/main/res/values/strings.xml diff --git a/kotlin/samples/containers/app-raven/src/main/res/values/styles.xml b/samples/containers/app-raven/src/main/res/values/styles.xml similarity index 100% rename from kotlin/samples/containers/app-raven/src/main/res/values/styles.xml rename to samples/containers/app-raven/src/main/res/values/styles.xml diff --git a/kotlin/samples/containers/common/build.gradle.kts b/samples/containers/common/build.gradle.kts similarity index 100% rename from kotlin/samples/containers/common/build.gradle.kts rename to samples/containers/common/build.gradle.kts diff --git a/kotlin/samples/containers/common/src/main/java/com/squareup/sample/container/overviewdetail/OverviewDetailScreen.kt b/samples/containers/common/src/main/java/com/squareup/sample/container/overviewdetail/OverviewDetailScreen.kt similarity index 100% rename from kotlin/samples/containers/common/src/main/java/com/squareup/sample/container/overviewdetail/OverviewDetailScreen.kt rename to samples/containers/common/src/main/java/com/squareup/sample/container/overviewdetail/OverviewDetailScreen.kt diff --git a/kotlin/samples/containers/common/src/main/java/com/squareup/sample/container/panel/PanelContainerScreen.kt b/samples/containers/common/src/main/java/com/squareup/sample/container/panel/PanelContainerScreen.kt similarity index 100% rename from kotlin/samples/containers/common/src/main/java/com/squareup/sample/container/panel/PanelContainerScreen.kt rename to samples/containers/common/src/main/java/com/squareup/sample/container/panel/PanelContainerScreen.kt diff --git a/kotlin/samples/containers/common/src/main/java/com/squareup/sample/container/panel/ScrimContainerScreen.kt b/samples/containers/common/src/main/java/com/squareup/sample/container/panel/ScrimContainerScreen.kt similarity index 100% rename from kotlin/samples/containers/common/src/main/java/com/squareup/sample/container/panel/ScrimContainerScreen.kt rename to samples/containers/common/src/main/java/com/squareup/sample/container/panel/ScrimContainerScreen.kt diff --git a/kotlin/samples/containers/common/src/test/java/com/squareup/sample/container/overviewdetail/OverviewDetailScreenTest.kt b/samples/containers/common/src/test/java/com/squareup/sample/container/overviewdetail/OverviewDetailScreenTest.kt similarity index 100% rename from kotlin/samples/containers/common/src/test/java/com/squareup/sample/container/overviewdetail/OverviewDetailScreenTest.kt rename to samples/containers/common/src/test/java/com/squareup/sample/container/overviewdetail/OverviewDetailScreenTest.kt diff --git a/kotlin/samples/containers/hello-back-button/build.gradle.kts b/samples/containers/hello-back-button/build.gradle.kts similarity index 100% rename from kotlin/samples/containers/hello-back-button/build.gradle.kts rename to samples/containers/hello-back-button/build.gradle.kts diff --git a/kotlin/samples/containers/hello-back-button/src/androidTest/java/com/squareup/sample/hellobackbutton/HelloBackButtonEspressoTest.kt b/samples/containers/hello-back-button/src/androidTest/java/com/squareup/sample/hellobackbutton/HelloBackButtonEspressoTest.kt similarity index 100% rename from kotlin/samples/containers/hello-back-button/src/androidTest/java/com/squareup/sample/hellobackbutton/HelloBackButtonEspressoTest.kt rename to samples/containers/hello-back-button/src/androidTest/java/com/squareup/sample/hellobackbutton/HelloBackButtonEspressoTest.kt diff --git a/kotlin/samples/containers/hello-back-button/src/main/AndroidManifest.xml b/samples/containers/hello-back-button/src/main/AndroidManifest.xml similarity index 100% rename from kotlin/samples/containers/hello-back-button/src/main/AndroidManifest.xml rename to samples/containers/hello-back-button/src/main/AndroidManifest.xml diff --git a/kotlin/samples/containers/hello-back-button/src/main/java/com/squareup/sample/hellobackbutton/AreYouSureWorkflow.kt b/samples/containers/hello-back-button/src/main/java/com/squareup/sample/hellobackbutton/AreYouSureWorkflow.kt similarity index 100% rename from kotlin/samples/containers/hello-back-button/src/main/java/com/squareup/sample/hellobackbutton/AreYouSureWorkflow.kt rename to samples/containers/hello-back-button/src/main/java/com/squareup/sample/hellobackbutton/AreYouSureWorkflow.kt diff --git a/kotlin/samples/containers/hello-back-button/src/main/java/com/squareup/sample/hellobackbutton/HelloBackButtonActivity.kt b/samples/containers/hello-back-button/src/main/java/com/squareup/sample/hellobackbutton/HelloBackButtonActivity.kt similarity index 100% rename from kotlin/samples/containers/hello-back-button/src/main/java/com/squareup/sample/hellobackbutton/HelloBackButtonActivity.kt rename to samples/containers/hello-back-button/src/main/java/com/squareup/sample/hellobackbutton/HelloBackButtonActivity.kt diff --git a/kotlin/samples/containers/hello-back-button/src/main/java/com/squareup/sample/hellobackbutton/HelloBackButtonLayoutRunner.kt b/samples/containers/hello-back-button/src/main/java/com/squareup/sample/hellobackbutton/HelloBackButtonLayoutRunner.kt similarity index 100% rename from kotlin/samples/containers/hello-back-button/src/main/java/com/squareup/sample/hellobackbutton/HelloBackButtonLayoutRunner.kt rename to samples/containers/hello-back-button/src/main/java/com/squareup/sample/hellobackbutton/HelloBackButtonLayoutRunner.kt diff --git a/kotlin/samples/containers/hello-back-button/src/main/java/com/squareup/sample/hellobackbutton/HelloBackButtonWorkflow.kt b/samples/containers/hello-back-button/src/main/java/com/squareup/sample/hellobackbutton/HelloBackButtonWorkflow.kt similarity index 100% rename from kotlin/samples/containers/hello-back-button/src/main/java/com/squareup/sample/hellobackbutton/HelloBackButtonWorkflow.kt rename to samples/containers/hello-back-button/src/main/java/com/squareup/sample/hellobackbutton/HelloBackButtonWorkflow.kt diff --git a/kotlin/samples/containers/hello-back-button/src/main/res/layout/hello_back_button_layout.xml b/samples/containers/hello-back-button/src/main/res/layout/hello_back_button_layout.xml similarity index 100% rename from kotlin/samples/containers/hello-back-button/src/main/res/layout/hello_back_button_layout.xml rename to samples/containers/hello-back-button/src/main/res/layout/hello_back_button_layout.xml diff --git a/kotlin/samples/containers/hello-back-button/src/main/res/values/strings.xml b/samples/containers/hello-back-button/src/main/res/values/strings.xml similarity index 100% rename from kotlin/samples/containers/hello-back-button/src/main/res/values/strings.xml rename to samples/containers/hello-back-button/src/main/res/values/strings.xml diff --git a/kotlin/samples/containers/hello-back-button/src/main/res/values/styles.xml b/samples/containers/hello-back-button/src/main/res/values/styles.xml similarity index 100% rename from kotlin/samples/containers/hello-back-button/src/main/res/values/styles.xml rename to samples/containers/hello-back-button/src/main/res/values/styles.xml diff --git a/kotlin/samples/containers/poetry/build.gradle.kts b/samples/containers/poetry/build.gradle.kts similarity index 100% rename from kotlin/samples/containers/poetry/build.gradle.kts rename to samples/containers/poetry/build.gradle.kts diff --git a/kotlin/samples/containers/poetry/src/main/AndroidManifest.xml b/samples/containers/poetry/src/main/AndroidManifest.xml similarity index 100% rename from kotlin/samples/containers/poetry/src/main/AndroidManifest.xml rename to samples/containers/poetry/src/main/AndroidManifest.xml diff --git a/kotlin/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/PoemWorkflow.kt b/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/PoemWorkflow.kt similarity index 100% rename from kotlin/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/PoemWorkflow.kt rename to samples/containers/poetry/src/main/java/com/squareup/sample/poetry/PoemWorkflow.kt diff --git a/kotlin/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/PoetryViews.kt b/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/PoetryViews.kt similarity index 100% rename from kotlin/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/PoetryViews.kt rename to samples/containers/poetry/src/main/java/com/squareup/sample/poetry/PoetryViews.kt diff --git a/kotlin/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/StanzaLayoutRunner.kt b/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/StanzaLayoutRunner.kt similarity index 100% rename from kotlin/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/StanzaLayoutRunner.kt rename to samples/containers/poetry/src/main/java/com/squareup/sample/poetry/StanzaLayoutRunner.kt diff --git a/kotlin/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/StanzaListLayoutRunner.kt b/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/StanzaListLayoutRunner.kt similarity index 100% rename from kotlin/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/StanzaListLayoutRunner.kt rename to samples/containers/poetry/src/main/java/com/squareup/sample/poetry/StanzaListLayoutRunner.kt diff --git a/kotlin/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/StanzaListWorkflow.kt b/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/StanzaListWorkflow.kt similarity index 100% rename from kotlin/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/StanzaListWorkflow.kt rename to samples/containers/poetry/src/main/java/com/squareup/sample/poetry/StanzaListWorkflow.kt diff --git a/kotlin/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/StanzaWorkflow.kt b/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/StanzaWorkflow.kt similarity index 100% rename from kotlin/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/StanzaWorkflow.kt rename to samples/containers/poetry/src/main/java/com/squareup/sample/poetry/StanzaWorkflow.kt diff --git a/kotlin/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/model/Poem.kt b/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/model/Poem.kt similarity index 100% rename from kotlin/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/model/Poem.kt rename to samples/containers/poetry/src/main/java/com/squareup/sample/poetry/model/Poem.kt diff --git a/kotlin/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/model/Poet.kt b/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/model/Poet.kt similarity index 100% rename from kotlin/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/model/Poet.kt rename to samples/containers/poetry/src/main/java/com/squareup/sample/poetry/model/Poet.kt diff --git a/kotlin/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/model/Raven.kt b/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/model/Raven.kt similarity index 100% rename from kotlin/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/model/Raven.kt rename to samples/containers/poetry/src/main/java/com/squareup/sample/poetry/model/Raven.kt diff --git a/kotlin/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/model/TheConquerorWorm.kt b/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/model/TheConquerorWorm.kt similarity index 100% rename from kotlin/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/model/TheConquerorWorm.kt rename to samples/containers/poetry/src/main/java/com/squareup/sample/poetry/model/TheConquerorWorm.kt diff --git a/kotlin/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/model/TheTyger.kt b/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/model/TheTyger.kt similarity index 100% rename from kotlin/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/model/TheTyger.kt rename to samples/containers/poetry/src/main/java/com/squareup/sample/poetry/model/TheTyger.kt diff --git a/kotlin/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/model/ToHelen.kt b/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/model/ToHelen.kt similarity index 100% rename from kotlin/samples/containers/poetry/src/main/java/com/squareup/sample/poetry/model/ToHelen.kt rename to samples/containers/poetry/src/main/java/com/squareup/sample/poetry/model/ToHelen.kt diff --git a/kotlin/samples/containers/poetry/src/main/res/drawable/list_selector.xml b/samples/containers/poetry/src/main/res/drawable/list_selector.xml similarity index 100% rename from kotlin/samples/containers/poetry/src/main/res/drawable/list_selector.xml rename to samples/containers/poetry/src/main/res/drawable/list_selector.xml diff --git a/kotlin/samples/containers/poetry/src/main/res/layout/list.xml b/samples/containers/poetry/src/main/res/layout/list.xml similarity index 100% rename from kotlin/samples/containers/poetry/src/main/res/layout/list.xml rename to samples/containers/poetry/src/main/res/layout/list.xml diff --git a/kotlin/samples/containers/poetry/src/main/res/layout/list_row_selectable.xml b/samples/containers/poetry/src/main/res/layout/list_row_selectable.xml similarity index 100% rename from kotlin/samples/containers/poetry/src/main/res/layout/list_row_selectable.xml rename to samples/containers/poetry/src/main/res/layout/list_row_selectable.xml diff --git a/kotlin/samples/containers/poetry/src/main/res/layout/list_row_unselectable.xml b/samples/containers/poetry/src/main/res/layout/list_row_unselectable.xml similarity index 100% rename from kotlin/samples/containers/poetry/src/main/res/layout/list_row_unselectable.xml rename to samples/containers/poetry/src/main/res/layout/list_row_unselectable.xml diff --git a/kotlin/samples/containers/poetry/src/main/res/layout/stanza_layout.xml b/samples/containers/poetry/src/main/res/layout/stanza_layout.xml similarity index 100% rename from kotlin/samples/containers/poetry/src/main/res/layout/stanza_layout.xml rename to samples/containers/poetry/src/main/res/layout/stanza_layout.xml diff --git a/kotlin/samples/containers/poetry/src/main/res/values/strings.xml b/samples/containers/poetry/src/main/res/values/strings.xml similarity index 100% rename from kotlin/samples/containers/poetry/src/main/res/values/strings.xml rename to samples/containers/poetry/src/main/res/values/strings.xml diff --git a/kotlin/samples/containers/poetry/src/main/res/values/styles.xml b/samples/containers/poetry/src/main/res/values/styles.xml similarity index 100% rename from kotlin/samples/containers/poetry/src/main/res/values/styles.xml rename to samples/containers/poetry/src/main/res/values/styles.xml diff --git a/kotlin/samples/dungeon/README.md b/samples/dungeon/README.md similarity index 100% rename from kotlin/samples/dungeon/README.md rename to samples/dungeon/README.md diff --git a/kotlin/samples/dungeon/app/build.gradle.kts b/samples/dungeon/app/build.gradle.kts similarity index 100% rename from kotlin/samples/dungeon/app/build.gradle.kts rename to samples/dungeon/app/build.gradle.kts diff --git a/kotlin/samples/dungeon/app/src/androidTest/java/com/squareup/sample/dungeon/DungeonAppTest.kt b/samples/dungeon/app/src/androidTest/java/com/squareup/sample/dungeon/DungeonAppTest.kt similarity index 100% rename from kotlin/samples/dungeon/app/src/androidTest/java/com/squareup/sample/dungeon/DungeonAppTest.kt rename to samples/dungeon/app/src/androidTest/java/com/squareup/sample/dungeon/DungeonAppTest.kt diff --git a/kotlin/samples/dungeon/app/src/androidTest/java/com/squareup/sample/dungeon/DungeonTestRunner.kt b/samples/dungeon/app/src/androidTest/java/com/squareup/sample/dungeon/DungeonTestRunner.kt similarity index 100% rename from kotlin/samples/dungeon/app/src/androidTest/java/com/squareup/sample/dungeon/DungeonTestRunner.kt rename to samples/dungeon/app/src/androidTest/java/com/squareup/sample/dungeon/DungeonTestRunner.kt diff --git a/kotlin/samples/dungeon/app/src/androidTest/java/com/squareup/sample/dungeon/TestApplication.kt b/samples/dungeon/app/src/androidTest/java/com/squareup/sample/dungeon/TestApplication.kt similarity index 100% rename from kotlin/samples/dungeon/app/src/androidTest/java/com/squareup/sample/dungeon/TestApplication.kt rename to samples/dungeon/app/src/androidTest/java/com/squareup/sample/dungeon/TestApplication.kt diff --git a/kotlin/samples/dungeon/app/src/main/AndroidManifest.xml b/samples/dungeon/app/src/main/AndroidManifest.xml similarity index 100% rename from kotlin/samples/dungeon/app/src/main/AndroidManifest.xml rename to samples/dungeon/app/src/main/AndroidManifest.xml diff --git a/kotlin/samples/dungeon/app/src/main/assets/boards/simple_board.txt b/samples/dungeon/app/src/main/assets/boards/simple_board.txt similarity index 100% rename from kotlin/samples/dungeon/app/src/main/assets/boards/simple_board.txt rename to samples/dungeon/app/src/main/assets/boards/simple_board.txt diff --git a/kotlin/samples/dungeon/app/src/main/assets/boards/simple_maze.txt b/samples/dungeon/app/src/main/assets/boards/simple_maze.txt similarity index 100% rename from kotlin/samples/dungeon/app/src/main/assets/boards/simple_maze.txt rename to samples/dungeon/app/src/main/assets/boards/simple_maze.txt diff --git a/kotlin/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/BoardLoader.kt b/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/BoardLoader.kt similarity index 100% rename from kotlin/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/BoardLoader.kt rename to samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/BoardLoader.kt diff --git a/kotlin/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/BoardView.kt b/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/BoardView.kt similarity index 100% rename from kotlin/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/BoardView.kt rename to samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/BoardView.kt diff --git a/kotlin/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/BoardsListLayoutRunner.kt b/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/BoardsListLayoutRunner.kt similarity index 100% rename from kotlin/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/BoardsListLayoutRunner.kt rename to samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/BoardsListLayoutRunner.kt diff --git a/kotlin/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/Component.kt b/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/Component.kt similarity index 100% rename from kotlin/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/Component.kt rename to samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/Component.kt diff --git a/kotlin/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/DungeonAppWorkflow.kt b/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/DungeonAppWorkflow.kt similarity index 100% rename from kotlin/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/DungeonAppWorkflow.kt rename to samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/DungeonAppWorkflow.kt diff --git a/kotlin/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/DungeonApplication.kt b/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/DungeonApplication.kt similarity index 100% rename from kotlin/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/DungeonApplication.kt rename to samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/DungeonApplication.kt diff --git a/kotlin/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/GameLayoutRunner.kt b/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/GameLayoutRunner.kt similarity index 100% rename from kotlin/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/GameLayoutRunner.kt rename to samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/GameLayoutRunner.kt diff --git a/kotlin/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/GameSessionWorkflow.kt b/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/GameSessionWorkflow.kt similarity index 100% rename from kotlin/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/GameSessionWorkflow.kt rename to samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/GameSessionWorkflow.kt diff --git a/kotlin/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/LoadingBinding.kt b/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/LoadingBinding.kt similarity index 100% rename from kotlin/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/LoadingBinding.kt rename to samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/LoadingBinding.kt diff --git a/kotlin/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/MainActivity.kt b/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/MainActivity.kt similarity index 100% rename from kotlin/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/MainActivity.kt rename to samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/MainActivity.kt diff --git a/kotlin/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/TimeMachineAppWorkflow.kt b/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/TimeMachineAppWorkflow.kt similarity index 100% rename from kotlin/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/TimeMachineAppWorkflow.kt rename to samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/TimeMachineAppWorkflow.kt diff --git a/kotlin/samples/dungeon/app/src/main/res/drawable/ic_chevron_left_black_24dp.xml b/samples/dungeon/app/src/main/res/drawable/ic_chevron_left_black_24dp.xml similarity index 100% rename from kotlin/samples/dungeon/app/src/main/res/drawable/ic_chevron_left_black_24dp.xml rename to samples/dungeon/app/src/main/res/drawable/ic_chevron_left_black_24dp.xml diff --git a/kotlin/samples/dungeon/app/src/main/res/layout/boards_list_item.xml b/samples/dungeon/app/src/main/res/layout/boards_list_item.xml similarity index 100% rename from kotlin/samples/dungeon/app/src/main/res/layout/boards_list_item.xml rename to samples/dungeon/app/src/main/res/layout/boards_list_item.xml diff --git a/kotlin/samples/dungeon/app/src/main/res/layout/boards_list_layout.xml b/samples/dungeon/app/src/main/res/layout/boards_list_layout.xml similarity index 98% rename from kotlin/samples/dungeon/app/src/main/res/layout/boards_list_layout.xml rename to samples/dungeon/app/src/main/res/layout/boards_list_layout.xml index c749868a5..e89f18b66 100644 --- a/kotlin/samples/dungeon/app/src/main/res/layout/boards_list_layout.xml +++ b/samples/dungeon/app/src/main/res/layout/boards_list_layout.xml @@ -39,4 +39,4 @@ app:spanCount="2" /> - \ No newline at end of file + diff --git a/kotlin/samples/dungeon/app/src/main/res/layout/game_layout.xml b/samples/dungeon/app/src/main/res/layout/game_layout.xml similarity index 100% rename from kotlin/samples/dungeon/app/src/main/res/layout/game_layout.xml rename to samples/dungeon/app/src/main/res/layout/game_layout.xml diff --git a/kotlin/samples/dungeon/app/src/main/res/layout/loading_layout.xml b/samples/dungeon/app/src/main/res/layout/loading_layout.xml similarity index 97% rename from kotlin/samples/dungeon/app/src/main/res/layout/loading_layout.xml rename to samples/dungeon/app/src/main/res/layout/loading_layout.xml index 79005e798..8c36e9dc7 100644 --- a/kotlin/samples/dungeon/app/src/main/res/layout/loading_layout.xml +++ b/samples/dungeon/app/src/main/res/layout/loading_layout.xml @@ -41,4 +41,4 @@ app:layout_constraintTop_toBottomOf="@+id/progressBar" tools:text="Loading…" /> - \ No newline at end of file + diff --git a/kotlin/samples/dungeon/app/src/main/res/values/colors.xml b/samples/dungeon/app/src/main/res/values/colors.xml similarity index 100% rename from kotlin/samples/dungeon/app/src/main/res/values/colors.xml rename to samples/dungeon/app/src/main/res/values/colors.xml diff --git a/kotlin/samples/dungeon/app/src/main/res/values/dimens.xml b/samples/dungeon/app/src/main/res/values/dimens.xml similarity index 98% rename from kotlin/samples/dungeon/app/src/main/res/values/dimens.xml rename to samples/dungeon/app/src/main/res/values/dimens.xml index 873183044..79cbd17a0 100644 --- a/kotlin/samples/dungeon/app/src/main/res/values/dimens.xml +++ b/samples/dungeon/app/src/main/res/values/dimens.xml @@ -16,4 +16,4 @@ --> 32dp - \ No newline at end of file + diff --git a/kotlin/samples/dungeon/app/src/main/res/values/strings.xml b/samples/dungeon/app/src/main/res/values/strings.xml similarity index 100% rename from kotlin/samples/dungeon/app/src/main/res/values/strings.xml rename to samples/dungeon/app/src/main/res/values/strings.xml diff --git a/kotlin/samples/dungeon/app/src/main/res/values/styles.xml b/samples/dungeon/app/src/main/res/values/styles.xml similarity index 100% rename from kotlin/samples/dungeon/app/src/main/res/values/styles.xml rename to samples/dungeon/app/src/main/res/values/styles.xml diff --git a/kotlin/samples/dungeon/common/build.gradle.kts b/samples/dungeon/common/build.gradle.kts similarity index 100% rename from kotlin/samples/dungeon/common/build.gradle.kts rename to samples/dungeon/common/build.gradle.kts diff --git a/kotlin/samples/dungeon/common/src/main/java/com/squareup/sample/dungeon/ActorWorkflow.kt b/samples/dungeon/common/src/main/java/com/squareup/sample/dungeon/ActorWorkflow.kt similarity index 100% rename from kotlin/samples/dungeon/common/src/main/java/com/squareup/sample/dungeon/ActorWorkflow.kt rename to samples/dungeon/common/src/main/java/com/squareup/sample/dungeon/ActorWorkflow.kt diff --git a/kotlin/samples/dungeon/common/src/main/java/com/squareup/sample/dungeon/AiWorkflow.kt b/samples/dungeon/common/src/main/java/com/squareup/sample/dungeon/AiWorkflow.kt similarity index 100% rename from kotlin/samples/dungeon/common/src/main/java/com/squareup/sample/dungeon/AiWorkflow.kt rename to samples/dungeon/common/src/main/java/com/squareup/sample/dungeon/AiWorkflow.kt diff --git a/kotlin/samples/dungeon/common/src/main/java/com/squareup/sample/dungeon/Direction.kt b/samples/dungeon/common/src/main/java/com/squareup/sample/dungeon/Direction.kt similarity index 100% rename from kotlin/samples/dungeon/common/src/main/java/com/squareup/sample/dungeon/Direction.kt rename to samples/dungeon/common/src/main/java/com/squareup/sample/dungeon/Direction.kt diff --git a/kotlin/samples/dungeon/common/src/main/java/com/squareup/sample/dungeon/Game.kt b/samples/dungeon/common/src/main/java/com/squareup/sample/dungeon/Game.kt similarity index 100% rename from kotlin/samples/dungeon/common/src/main/java/com/squareup/sample/dungeon/Game.kt rename to samples/dungeon/common/src/main/java/com/squareup/sample/dungeon/Game.kt diff --git a/kotlin/samples/dungeon/common/src/main/java/com/squareup/sample/dungeon/GameWorkflow.kt b/samples/dungeon/common/src/main/java/com/squareup/sample/dungeon/GameWorkflow.kt similarity index 100% rename from kotlin/samples/dungeon/common/src/main/java/com/squareup/sample/dungeon/GameWorkflow.kt rename to samples/dungeon/common/src/main/java/com/squareup/sample/dungeon/GameWorkflow.kt diff --git a/kotlin/samples/dungeon/common/src/main/java/com/squareup/sample/dungeon/Movement.kt b/samples/dungeon/common/src/main/java/com/squareup/sample/dungeon/Movement.kt similarity index 100% rename from kotlin/samples/dungeon/common/src/main/java/com/squareup/sample/dungeon/Movement.kt rename to samples/dungeon/common/src/main/java/com/squareup/sample/dungeon/Movement.kt diff --git a/kotlin/samples/dungeon/common/src/main/java/com/squareup/sample/dungeon/PlayerWorkflow.kt b/samples/dungeon/common/src/main/java/com/squareup/sample/dungeon/PlayerWorkflow.kt similarity index 100% rename from kotlin/samples/dungeon/common/src/main/java/com/squareup/sample/dungeon/PlayerWorkflow.kt rename to samples/dungeon/common/src/main/java/com/squareup/sample/dungeon/PlayerWorkflow.kt diff --git a/kotlin/samples/dungeon/common/src/main/java/com/squareup/sample/dungeon/board/Board.kt b/samples/dungeon/common/src/main/java/com/squareup/sample/dungeon/board/Board.kt similarity index 100% rename from kotlin/samples/dungeon/common/src/main/java/com/squareup/sample/dungeon/board/Board.kt rename to samples/dungeon/common/src/main/java/com/squareup/sample/dungeon/board/Board.kt diff --git a/kotlin/samples/dungeon/common/src/main/java/com/squareup/sample/dungeon/board/BoardCell.kt b/samples/dungeon/common/src/main/java/com/squareup/sample/dungeon/board/BoardCell.kt similarity index 100% rename from kotlin/samples/dungeon/common/src/main/java/com/squareup/sample/dungeon/board/BoardCell.kt rename to samples/dungeon/common/src/main/java/com/squareup/sample/dungeon/board/BoardCell.kt diff --git a/kotlin/samples/dungeon/common/src/main/java/com/squareup/sample/dungeon/board/Parser.kt b/samples/dungeon/common/src/main/java/com/squareup/sample/dungeon/board/Parser.kt similarity index 100% rename from kotlin/samples/dungeon/common/src/main/java/com/squareup/sample/dungeon/board/Parser.kt rename to samples/dungeon/common/src/main/java/com/squareup/sample/dungeon/board/Parser.kt diff --git a/kotlin/samples/dungeon/common/src/test/java/com/squareup/sample/dungeon/MovementTest.kt b/samples/dungeon/common/src/test/java/com/squareup/sample/dungeon/MovementTest.kt similarity index 100% rename from kotlin/samples/dungeon/common/src/test/java/com/squareup/sample/dungeon/MovementTest.kt rename to samples/dungeon/common/src/test/java/com/squareup/sample/dungeon/MovementTest.kt diff --git a/kotlin/samples/dungeon/common/src/test/java/com/squareup/sample/dungeon/board/ParserTest.kt b/samples/dungeon/common/src/test/java/com/squareup/sample/dungeon/board/ParserTest.kt similarity index 100% rename from kotlin/samples/dungeon/common/src/test/java/com/squareup/sample/dungeon/board/ParserTest.kt rename to samples/dungeon/common/src/test/java/com/squareup/sample/dungeon/board/ParserTest.kt diff --git a/kotlin/samples/dungeon/timemachine-shakeable/build.gradle.kts b/samples/dungeon/timemachine-shakeable/build.gradle.kts similarity index 100% rename from kotlin/samples/dungeon/timemachine-shakeable/build.gradle.kts rename to samples/dungeon/timemachine-shakeable/build.gradle.kts diff --git a/kotlin/samples/dungeon/timemachine-shakeable/src/main/AndroidManifest.xml b/samples/dungeon/timemachine-shakeable/src/main/AndroidManifest.xml similarity index 100% rename from kotlin/samples/dungeon/timemachine-shakeable/src/main/AndroidManifest.xml rename to samples/dungeon/timemachine-shakeable/src/main/AndroidManifest.xml diff --git a/kotlin/samples/dungeon/timemachine-shakeable/src/main/java/com/squareup/sample/timemachine/shakeable/ShakeWorker.kt b/samples/dungeon/timemachine-shakeable/src/main/java/com/squareup/sample/timemachine/shakeable/ShakeWorker.kt similarity index 100% rename from kotlin/samples/dungeon/timemachine-shakeable/src/main/java/com/squareup/sample/timemachine/shakeable/ShakeWorker.kt rename to samples/dungeon/timemachine-shakeable/src/main/java/com/squareup/sample/timemachine/shakeable/ShakeWorker.kt diff --git a/kotlin/samples/dungeon/timemachine-shakeable/src/main/java/com/squareup/sample/timemachine/shakeable/ShakeableTimeMachineLayoutRunner.kt b/samples/dungeon/timemachine-shakeable/src/main/java/com/squareup/sample/timemachine/shakeable/ShakeableTimeMachineLayoutRunner.kt similarity index 100% rename from kotlin/samples/dungeon/timemachine-shakeable/src/main/java/com/squareup/sample/timemachine/shakeable/ShakeableTimeMachineLayoutRunner.kt rename to samples/dungeon/timemachine-shakeable/src/main/java/com/squareup/sample/timemachine/shakeable/ShakeableTimeMachineLayoutRunner.kt diff --git a/kotlin/samples/dungeon/timemachine-shakeable/src/main/java/com/squareup/sample/timemachine/shakeable/ShakeableTimeMachineRendering.kt b/samples/dungeon/timemachine-shakeable/src/main/java/com/squareup/sample/timemachine/shakeable/ShakeableTimeMachineRendering.kt similarity index 100% rename from kotlin/samples/dungeon/timemachine-shakeable/src/main/java/com/squareup/sample/timemachine/shakeable/ShakeableTimeMachineRendering.kt rename to samples/dungeon/timemachine-shakeable/src/main/java/com/squareup/sample/timemachine/shakeable/ShakeableTimeMachineRendering.kt diff --git a/kotlin/samples/dungeon/timemachine-shakeable/src/main/java/com/squareup/sample/timemachine/shakeable/ShakeableTimeMachineWorkflow.kt b/samples/dungeon/timemachine-shakeable/src/main/java/com/squareup/sample/timemachine/shakeable/ShakeableTimeMachineWorkflow.kt similarity index 100% rename from kotlin/samples/dungeon/timemachine-shakeable/src/main/java/com/squareup/sample/timemachine/shakeable/ShakeableTimeMachineWorkflow.kt rename to samples/dungeon/timemachine-shakeable/src/main/java/com/squareup/sample/timemachine/shakeable/ShakeableTimeMachineWorkflow.kt diff --git a/kotlin/samples/dungeon/timemachine-shakeable/src/main/java/com/squareup/sample/timemachine/shakeable/internal/GlassFrameLayout.kt b/samples/dungeon/timemachine-shakeable/src/main/java/com/squareup/sample/timemachine/shakeable/internal/GlassFrameLayout.kt similarity index 100% rename from kotlin/samples/dungeon/timemachine-shakeable/src/main/java/com/squareup/sample/timemachine/shakeable/internal/GlassFrameLayout.kt rename to samples/dungeon/timemachine-shakeable/src/main/java/com/squareup/sample/timemachine/shakeable/internal/GlassFrameLayout.kt diff --git a/kotlin/samples/dungeon/timemachine-shakeable/src/main/res/layout/shakeable_time_machine_layout.xml b/samples/dungeon/timemachine-shakeable/src/main/res/layout/shakeable_time_machine_layout.xml similarity index 100% rename from kotlin/samples/dungeon/timemachine-shakeable/src/main/res/layout/shakeable_time_machine_layout.xml rename to samples/dungeon/timemachine-shakeable/src/main/res/layout/shakeable_time_machine_layout.xml diff --git a/kotlin/samples/dungeon/timemachine-shakeable/src/main/res/values/dimens.xml b/samples/dungeon/timemachine-shakeable/src/main/res/values/dimens.xml similarity index 100% rename from kotlin/samples/dungeon/timemachine-shakeable/src/main/res/values/dimens.xml rename to samples/dungeon/timemachine-shakeable/src/main/res/values/dimens.xml diff --git a/kotlin/samples/dungeon/timemachine-shakeable/src/main/res/values/strings.xml b/samples/dungeon/timemachine-shakeable/src/main/res/values/strings.xml similarity index 100% rename from kotlin/samples/dungeon/timemachine-shakeable/src/main/res/values/strings.xml rename to samples/dungeon/timemachine-shakeable/src/main/res/values/strings.xml diff --git a/kotlin/samples/dungeon/timemachine/build.gradle.kts b/samples/dungeon/timemachine/build.gradle.kts similarity index 100% rename from kotlin/samples/dungeon/timemachine/build.gradle.kts rename to samples/dungeon/timemachine/build.gradle.kts diff --git a/kotlin/samples/dungeon/timemachine/src/main/java/com/squareup/sample/timemachine/RecorderWorkflow.kt b/samples/dungeon/timemachine/src/main/java/com/squareup/sample/timemachine/RecorderWorkflow.kt similarity index 100% rename from kotlin/samples/dungeon/timemachine/src/main/java/com/squareup/sample/timemachine/RecorderWorkflow.kt rename to samples/dungeon/timemachine/src/main/java/com/squareup/sample/timemachine/RecorderWorkflow.kt diff --git a/kotlin/samples/dungeon/timemachine/src/main/java/com/squareup/sample/timemachine/TimeMachineRendering.kt b/samples/dungeon/timemachine/src/main/java/com/squareup/sample/timemachine/TimeMachineRendering.kt similarity index 100% rename from kotlin/samples/dungeon/timemachine/src/main/java/com/squareup/sample/timemachine/TimeMachineRendering.kt rename to samples/dungeon/timemachine/src/main/java/com/squareup/sample/timemachine/TimeMachineRendering.kt diff --git a/kotlin/samples/dungeon/timemachine/src/main/java/com/squareup/sample/timemachine/TimeMachineWorkflow.kt b/samples/dungeon/timemachine/src/main/java/com/squareup/sample/timemachine/TimeMachineWorkflow.kt similarity index 100% rename from kotlin/samples/dungeon/timemachine/src/main/java/com/squareup/sample/timemachine/TimeMachineWorkflow.kt rename to samples/dungeon/timemachine/src/main/java/com/squareup/sample/timemachine/TimeMachineWorkflow.kt diff --git a/kotlin/samples/dungeon/timemachine/src/main/java/com/squareup/sample/timemachine/TimeSeries.kt b/samples/dungeon/timemachine/src/main/java/com/squareup/sample/timemachine/TimeSeries.kt similarity index 100% rename from kotlin/samples/dungeon/timemachine/src/main/java/com/squareup/sample/timemachine/TimeSeries.kt rename to samples/dungeon/timemachine/src/main/java/com/squareup/sample/timemachine/TimeSeries.kt diff --git a/kotlin/samples/dungeon/timemachine/src/test/java/com/squareup/sample/timemachine/RecorderWorkflowTest.kt b/samples/dungeon/timemachine/src/test/java/com/squareup/sample/timemachine/RecorderWorkflowTest.kt similarity index 100% rename from kotlin/samples/dungeon/timemachine/src/test/java/com/squareup/sample/timemachine/RecorderWorkflowTest.kt rename to samples/dungeon/timemachine/src/test/java/com/squareup/sample/timemachine/RecorderWorkflowTest.kt diff --git a/kotlin/samples/dungeon/timemachine/src/test/java/com/squareup/sample/timemachine/TimeMachineWorkflowTest.kt b/samples/dungeon/timemachine/src/test/java/com/squareup/sample/timemachine/TimeMachineWorkflowTest.kt similarity index 100% rename from kotlin/samples/dungeon/timemachine/src/test/java/com/squareup/sample/timemachine/TimeMachineWorkflowTest.kt rename to samples/dungeon/timemachine/src/test/java/com/squareup/sample/timemachine/TimeMachineWorkflowTest.kt diff --git a/kotlin/samples/dungeon/timemachine/src/test/java/com/squareup/sample/timemachine/TimeSeriesTest.kt b/samples/dungeon/timemachine/src/test/java/com/squareup/sample/timemachine/TimeSeriesTest.kt similarity index 100% rename from kotlin/samples/dungeon/timemachine/src/test/java/com/squareup/sample/timemachine/TimeSeriesTest.kt rename to samples/dungeon/timemachine/src/test/java/com/squareup/sample/timemachine/TimeSeriesTest.kt diff --git a/kotlin/samples/hello-terminal/.assets/hello-terminal-demo.gif b/samples/hello-terminal/.assets/hello-terminal-demo.gif similarity index 100% rename from kotlin/samples/hello-terminal/.assets/hello-terminal-demo.gif rename to samples/hello-terminal/.assets/hello-terminal-demo.gif diff --git a/kotlin/samples/hello-terminal/.assets/todo-terminal-demo.gif b/samples/hello-terminal/.assets/todo-terminal-demo.gif similarity index 100% rename from kotlin/samples/hello-terminal/.assets/todo-terminal-demo.gif rename to samples/hello-terminal/.assets/todo-terminal-demo.gif diff --git a/kotlin/samples/hello-terminal/README.md b/samples/hello-terminal/README.md similarity index 99% rename from kotlin/samples/hello-terminal/README.md rename to samples/hello-terminal/README.md index 3480b4381..19e66cbe3 100644 --- a/kotlin/samples/hello-terminal/README.md +++ b/samples/hello-terminal/README.md @@ -15,7 +15,6 @@ exit code. This module delegates to the third-party library [Lanterna](https://github.com/mabe02/lanterna) to do the actual hard work of talking to the system terminal. - ## hello-terminal-app Sample app demonstrating one possible way of writing terminal applications with workflows. @@ -25,7 +24,6 @@ Run with `./gradlew :samples:hello-terminal:hello-terminal-app:run` ![Screen recording of the sample app](.assets/hello-terminal-demo.gif) - ## todo-terminal-app Sample app that uses the sample `terminal-workflow` library to build a really simple TODO app that diff --git a/kotlin/samples/hello-terminal/hello-terminal-app/build.gradle.kts b/samples/hello-terminal/hello-terminal-app/build.gradle.kts similarity index 100% rename from kotlin/samples/hello-terminal/hello-terminal-app/build.gradle.kts rename to samples/hello-terminal/hello-terminal-app/build.gradle.kts diff --git a/kotlin/samples/hello-terminal/hello-terminal-app/src/main/java/com/squareup/sample/helloterminal/BlinkingCursorWorkflow.kt b/samples/hello-terminal/hello-terminal-app/src/main/java/com/squareup/sample/helloterminal/BlinkingCursorWorkflow.kt similarity index 100% rename from kotlin/samples/hello-terminal/hello-terminal-app/src/main/java/com/squareup/sample/helloterminal/BlinkingCursorWorkflow.kt rename to samples/hello-terminal/hello-terminal-app/src/main/java/com/squareup/sample/helloterminal/BlinkingCursorWorkflow.kt diff --git a/kotlin/samples/hello-terminal/hello-terminal-app/src/main/java/com/squareup/sample/helloterminal/HelloTerminalWorkflow.kt b/samples/hello-terminal/hello-terminal-app/src/main/java/com/squareup/sample/helloterminal/HelloTerminalWorkflow.kt similarity index 100% rename from kotlin/samples/hello-terminal/hello-terminal-app/src/main/java/com/squareup/sample/helloterminal/HelloTerminalWorkflow.kt rename to samples/hello-terminal/hello-terminal-app/src/main/java/com/squareup/sample/helloterminal/HelloTerminalWorkflow.kt diff --git a/kotlin/samples/hello-terminal/hello-terminal-app/src/main/java/com/squareup/sample/helloterminal/Main.kt b/samples/hello-terminal/hello-terminal-app/src/main/java/com/squareup/sample/helloterminal/Main.kt similarity index 100% rename from kotlin/samples/hello-terminal/hello-terminal-app/src/main/java/com/squareup/sample/helloterminal/Main.kt rename to samples/hello-terminal/hello-terminal-app/src/main/java/com/squareup/sample/helloterminal/Main.kt diff --git a/kotlin/samples/hello-terminal/terminal-workflow/build.gradle.kts b/samples/hello-terminal/terminal-workflow/build.gradle.kts similarity index 100% rename from kotlin/samples/hello-terminal/terminal-workflow/build.gradle.kts rename to samples/hello-terminal/terminal-workflow/build.gradle.kts diff --git a/kotlin/samples/hello-terminal/terminal-workflow/src/main/java/com/squareup/sample/helloterminal/terminalworkflow/KeyStroke.kt b/samples/hello-terminal/terminal-workflow/src/main/java/com/squareup/sample/helloterminal/terminalworkflow/KeyStroke.kt similarity index 100% rename from kotlin/samples/hello-terminal/terminal-workflow/src/main/java/com/squareup/sample/helloterminal/terminalworkflow/KeyStroke.kt rename to samples/hello-terminal/terminal-workflow/src/main/java/com/squareup/sample/helloterminal/terminalworkflow/KeyStroke.kt diff --git a/kotlin/samples/hello-terminal/terminal-workflow/src/main/java/com/squareup/sample/helloterminal/terminalworkflow/TerminalProps.kt b/samples/hello-terminal/terminal-workflow/src/main/java/com/squareup/sample/helloterminal/terminalworkflow/TerminalProps.kt similarity index 100% rename from kotlin/samples/hello-terminal/terminal-workflow/src/main/java/com/squareup/sample/helloterminal/terminalworkflow/TerminalProps.kt rename to samples/hello-terminal/terminal-workflow/src/main/java/com/squareup/sample/helloterminal/terminalworkflow/TerminalProps.kt diff --git a/kotlin/samples/hello-terminal/terminal-workflow/src/main/java/com/squareup/sample/helloterminal/terminalworkflow/TerminalRendering.kt b/samples/hello-terminal/terminal-workflow/src/main/java/com/squareup/sample/helloterminal/terminalworkflow/TerminalRendering.kt similarity index 100% rename from kotlin/samples/hello-terminal/terminal-workflow/src/main/java/com/squareup/sample/helloterminal/terminalworkflow/TerminalRendering.kt rename to samples/hello-terminal/terminal-workflow/src/main/java/com/squareup/sample/helloterminal/terminalworkflow/TerminalRendering.kt diff --git a/kotlin/samples/hello-terminal/terminal-workflow/src/main/java/com/squareup/sample/helloterminal/terminalworkflow/TerminalSize.kt b/samples/hello-terminal/terminal-workflow/src/main/java/com/squareup/sample/helloterminal/terminalworkflow/TerminalSize.kt similarity index 100% rename from kotlin/samples/hello-terminal/terminal-workflow/src/main/java/com/squareup/sample/helloterminal/terminalworkflow/TerminalSize.kt rename to samples/hello-terminal/terminal-workflow/src/main/java/com/squareup/sample/helloterminal/terminalworkflow/TerminalSize.kt diff --git a/kotlin/samples/hello-terminal/terminal-workflow/src/main/java/com/squareup/sample/helloterminal/terminalworkflow/TerminalWorkflow.kt b/samples/hello-terminal/terminal-workflow/src/main/java/com/squareup/sample/helloterminal/terminalworkflow/TerminalWorkflow.kt similarity index 100% rename from kotlin/samples/hello-terminal/terminal-workflow/src/main/java/com/squareup/sample/helloterminal/terminalworkflow/TerminalWorkflow.kt rename to samples/hello-terminal/terminal-workflow/src/main/java/com/squareup/sample/helloterminal/terminalworkflow/TerminalWorkflow.kt diff --git a/kotlin/samples/hello-terminal/terminal-workflow/src/main/java/com/squareup/sample/helloterminal/terminalworkflow/TerminalWorkflowRunner.kt b/samples/hello-terminal/terminal-workflow/src/main/java/com/squareup/sample/helloterminal/terminalworkflow/TerminalWorkflowRunner.kt similarity index 100% rename from kotlin/samples/hello-terminal/terminal-workflow/src/main/java/com/squareup/sample/helloterminal/terminalworkflow/TerminalWorkflowRunner.kt rename to samples/hello-terminal/terminal-workflow/src/main/java/com/squareup/sample/helloterminal/terminalworkflow/TerminalWorkflowRunner.kt diff --git a/kotlin/samples/hello-terminal/todo-terminal-app/build.gradle.kts b/samples/hello-terminal/todo-terminal-app/build.gradle.kts similarity index 100% rename from kotlin/samples/hello-terminal/todo-terminal-app/build.gradle.kts rename to samples/hello-terminal/todo-terminal-app/build.gradle.kts diff --git a/kotlin/samples/hello-terminal/todo-terminal-app/src/main/java/com/squareup/sample/hellotodo/EditTextWorkflow.kt b/samples/hello-terminal/todo-terminal-app/src/main/java/com/squareup/sample/hellotodo/EditTextWorkflow.kt similarity index 100% rename from kotlin/samples/hello-terminal/todo-terminal-app/src/main/java/com/squareup/sample/hellotodo/EditTextWorkflow.kt rename to samples/hello-terminal/todo-terminal-app/src/main/java/com/squareup/sample/hellotodo/EditTextWorkflow.kt diff --git a/kotlin/samples/hello-terminal/todo-terminal-app/src/main/java/com/squareup/sample/hellotodo/Main.kt b/samples/hello-terminal/todo-terminal-app/src/main/java/com/squareup/sample/hellotodo/Main.kt similarity index 100% rename from kotlin/samples/hello-terminal/todo-terminal-app/src/main/java/com/squareup/sample/hellotodo/Main.kt rename to samples/hello-terminal/todo-terminal-app/src/main/java/com/squareup/sample/hellotodo/Main.kt diff --git a/kotlin/samples/hello-terminal/todo-terminal-app/src/main/java/com/squareup/sample/hellotodo/TodoWorkflow.kt b/samples/hello-terminal/todo-terminal-app/src/main/java/com/squareup/sample/hellotodo/TodoWorkflow.kt similarity index 100% rename from kotlin/samples/hello-terminal/todo-terminal-app/src/main/java/com/squareup/sample/hellotodo/TodoWorkflow.kt rename to samples/hello-terminal/todo-terminal-app/src/main/java/com/squareup/sample/hellotodo/TodoWorkflow.kt diff --git a/kotlin/samples/hello-workflow-fragment/build.gradle.kts b/samples/hello-workflow-fragment/build.gradle.kts similarity index 100% rename from kotlin/samples/hello-workflow-fragment/build.gradle.kts rename to samples/hello-workflow-fragment/build.gradle.kts diff --git a/kotlin/samples/hello-workflow-fragment/src/androidTest/java/com/squareup/sample/helloworkflowfragment/HelloWorkflowFragmentAppTest.kt b/samples/hello-workflow-fragment/src/androidTest/java/com/squareup/sample/helloworkflowfragment/HelloWorkflowFragmentAppTest.kt similarity index 100% rename from kotlin/samples/hello-workflow-fragment/src/androidTest/java/com/squareup/sample/helloworkflowfragment/HelloWorkflowFragmentAppTest.kt rename to samples/hello-workflow-fragment/src/androidTest/java/com/squareup/sample/helloworkflowfragment/HelloWorkflowFragmentAppTest.kt diff --git a/kotlin/samples/hello-workflow-fragment/src/main/AndroidManifest.xml b/samples/hello-workflow-fragment/src/main/AndroidManifest.xml similarity index 100% rename from kotlin/samples/hello-workflow-fragment/src/main/AndroidManifest.xml rename to samples/hello-workflow-fragment/src/main/AndroidManifest.xml diff --git a/kotlin/samples/hello-workflow-fragment/src/main/java/com/squareup/sample/helloworkflowfragment/HelloFragmentViewFactory.kt b/samples/hello-workflow-fragment/src/main/java/com/squareup/sample/helloworkflowfragment/HelloFragmentViewFactory.kt similarity index 100% rename from kotlin/samples/hello-workflow-fragment/src/main/java/com/squareup/sample/helloworkflowfragment/HelloFragmentViewFactory.kt rename to samples/hello-workflow-fragment/src/main/java/com/squareup/sample/helloworkflowfragment/HelloFragmentViewFactory.kt diff --git a/kotlin/samples/hello-workflow-fragment/src/main/java/com/squareup/sample/helloworkflowfragment/HelloWorkflow.kt b/samples/hello-workflow-fragment/src/main/java/com/squareup/sample/helloworkflowfragment/HelloWorkflow.kt similarity index 100% rename from kotlin/samples/hello-workflow-fragment/src/main/java/com/squareup/sample/helloworkflowfragment/HelloWorkflow.kt rename to samples/hello-workflow-fragment/src/main/java/com/squareup/sample/helloworkflowfragment/HelloWorkflow.kt diff --git a/kotlin/samples/hello-workflow-fragment/src/main/java/com/squareup/sample/helloworkflowfragment/HelloWorkflowFragment.kt b/samples/hello-workflow-fragment/src/main/java/com/squareup/sample/helloworkflowfragment/HelloWorkflowFragment.kt similarity index 100% rename from kotlin/samples/hello-workflow-fragment/src/main/java/com/squareup/sample/helloworkflowfragment/HelloWorkflowFragment.kt rename to samples/hello-workflow-fragment/src/main/java/com/squareup/sample/helloworkflowfragment/HelloWorkflowFragment.kt diff --git a/kotlin/samples/hello-workflow-fragment/src/main/java/com/squareup/sample/helloworkflowfragment/HelloWorkflowFragmentActivity.kt b/samples/hello-workflow-fragment/src/main/java/com/squareup/sample/helloworkflowfragment/HelloWorkflowFragmentActivity.kt similarity index 100% rename from kotlin/samples/hello-workflow-fragment/src/main/java/com/squareup/sample/helloworkflowfragment/HelloWorkflowFragmentActivity.kt rename to samples/hello-workflow-fragment/src/main/java/com/squareup/sample/helloworkflowfragment/HelloWorkflowFragmentActivity.kt diff --git a/kotlin/samples/hello-workflow-fragment/src/main/res/layout/hello_goodbye_layout.xml b/samples/hello-workflow-fragment/src/main/res/layout/hello_goodbye_layout.xml similarity index 100% rename from kotlin/samples/hello-workflow-fragment/src/main/res/layout/hello_goodbye_layout.xml rename to samples/hello-workflow-fragment/src/main/res/layout/hello_goodbye_layout.xml diff --git a/kotlin/samples/hello-workflow-fragment/src/main/res/layout/hello_workflow_fragment.xml b/samples/hello-workflow-fragment/src/main/res/layout/hello_workflow_fragment.xml similarity index 100% rename from kotlin/samples/hello-workflow-fragment/src/main/res/layout/hello_workflow_fragment.xml rename to samples/hello-workflow-fragment/src/main/res/layout/hello_workflow_fragment.xml diff --git a/kotlin/samples/hello-workflow-fragment/src/main/res/values/strings.xml b/samples/hello-workflow-fragment/src/main/res/values/strings.xml similarity index 100% rename from kotlin/samples/hello-workflow-fragment/src/main/res/values/strings.xml rename to samples/hello-workflow-fragment/src/main/res/values/strings.xml diff --git a/kotlin/samples/hello-workflow-fragment/src/main/res/values/styles.xml b/samples/hello-workflow-fragment/src/main/res/values/styles.xml similarity index 100% rename from kotlin/samples/hello-workflow-fragment/src/main/res/values/styles.xml rename to samples/hello-workflow-fragment/src/main/res/values/styles.xml diff --git a/kotlin/samples/hello-workflow/build.gradle.kts b/samples/hello-workflow/build.gradle.kts similarity index 100% rename from kotlin/samples/hello-workflow/build.gradle.kts rename to samples/hello-workflow/build.gradle.kts diff --git a/kotlin/samples/hello-workflow/src/androidTest/java/com/squareup/sample/helloworkflow/HelloWorkflowAppTest.kt b/samples/hello-workflow/src/androidTest/java/com/squareup/sample/helloworkflow/HelloWorkflowAppTest.kt similarity index 100% rename from kotlin/samples/hello-workflow/src/androidTest/java/com/squareup/sample/helloworkflow/HelloWorkflowAppTest.kt rename to samples/hello-workflow/src/androidTest/java/com/squareup/sample/helloworkflow/HelloWorkflowAppTest.kt diff --git a/kotlin/samples/hello-workflow/src/main/AndroidManifest.xml b/samples/hello-workflow/src/main/AndroidManifest.xml similarity index 100% rename from kotlin/samples/hello-workflow/src/main/AndroidManifest.xml rename to samples/hello-workflow/src/main/AndroidManifest.xml diff --git a/kotlin/samples/hello-workflow/src/main/java/com/squareup/sample/helloworkflow/HelloViewFactory.kt b/samples/hello-workflow/src/main/java/com/squareup/sample/helloworkflow/HelloViewFactory.kt similarity index 100% rename from kotlin/samples/hello-workflow/src/main/java/com/squareup/sample/helloworkflow/HelloViewFactory.kt rename to samples/hello-workflow/src/main/java/com/squareup/sample/helloworkflow/HelloViewFactory.kt diff --git a/kotlin/samples/hello-workflow/src/main/java/com/squareup/sample/helloworkflow/HelloWorkflow.kt b/samples/hello-workflow/src/main/java/com/squareup/sample/helloworkflow/HelloWorkflow.kt similarity index 100% rename from kotlin/samples/hello-workflow/src/main/java/com/squareup/sample/helloworkflow/HelloWorkflow.kt rename to samples/hello-workflow/src/main/java/com/squareup/sample/helloworkflow/HelloWorkflow.kt diff --git a/kotlin/samples/hello-workflow/src/main/java/com/squareup/sample/helloworkflow/HelloWorkflowActivity.kt b/samples/hello-workflow/src/main/java/com/squareup/sample/helloworkflow/HelloWorkflowActivity.kt similarity index 100% rename from kotlin/samples/hello-workflow/src/main/java/com/squareup/sample/helloworkflow/HelloWorkflowActivity.kt rename to samples/hello-workflow/src/main/java/com/squareup/sample/helloworkflow/HelloWorkflowActivity.kt diff --git a/kotlin/samples/hello-workflow/src/main/res/layout/hello_goodbye_layout.xml b/samples/hello-workflow/src/main/res/layout/hello_goodbye_layout.xml similarity index 100% rename from kotlin/samples/hello-workflow/src/main/res/layout/hello_goodbye_layout.xml rename to samples/hello-workflow/src/main/res/layout/hello_goodbye_layout.xml diff --git a/kotlin/samples/hello-workflow/src/main/res/values/strings.xml b/samples/hello-workflow/src/main/res/values/strings.xml similarity index 100% rename from kotlin/samples/hello-workflow/src/main/res/values/strings.xml rename to samples/hello-workflow/src/main/res/values/strings.xml diff --git a/kotlin/samples/hello-workflow/src/main/res/values/styles.xml b/samples/hello-workflow/src/main/res/values/styles.xml similarity index 100% rename from kotlin/samples/hello-workflow/src/main/res/values/styles.xml rename to samples/hello-workflow/src/main/res/values/styles.xml diff --git a/kotlin/samples/recyclerview/build.gradle.kts b/samples/recyclerview/build.gradle.kts similarity index 100% rename from kotlin/samples/recyclerview/build.gradle.kts rename to samples/recyclerview/build.gradle.kts diff --git a/kotlin/samples/recyclerview/src/androidTest/java/com/squareup/sample/recyclerview/RecyclerViewAppTest.kt b/samples/recyclerview/src/androidTest/java/com/squareup/sample/recyclerview/RecyclerViewAppTest.kt similarity index 100% rename from kotlin/samples/recyclerview/src/androidTest/java/com/squareup/sample/recyclerview/RecyclerViewAppTest.kt rename to samples/recyclerview/src/androidTest/java/com/squareup/sample/recyclerview/RecyclerViewAppTest.kt diff --git a/kotlin/samples/recyclerview/src/main/AndroidManifest.xml b/samples/recyclerview/src/main/AndroidManifest.xml similarity index 100% rename from kotlin/samples/recyclerview/src/main/AndroidManifest.xml rename to samples/recyclerview/src/main/AndroidManifest.xml diff --git a/kotlin/samples/recyclerview/src/main/java/com/squareup/sample/recyclerview/AddRowContainer.kt b/samples/recyclerview/src/main/java/com/squareup/sample/recyclerview/AddRowContainer.kt similarity index 100% rename from kotlin/samples/recyclerview/src/main/java/com/squareup/sample/recyclerview/AddRowContainer.kt rename to samples/recyclerview/src/main/java/com/squareup/sample/recyclerview/AddRowContainer.kt diff --git a/kotlin/samples/recyclerview/src/main/java/com/squareup/sample/recyclerview/AppWorkflow.kt b/samples/recyclerview/src/main/java/com/squareup/sample/recyclerview/AppWorkflow.kt similarity index 100% rename from kotlin/samples/recyclerview/src/main/java/com/squareup/sample/recyclerview/AppWorkflow.kt rename to samples/recyclerview/src/main/java/com/squareup/sample/recyclerview/AppWorkflow.kt diff --git a/kotlin/samples/recyclerview/src/main/java/com/squareup/sample/recyclerview/BaseScreenViewFactory.kt b/samples/recyclerview/src/main/java/com/squareup/sample/recyclerview/BaseScreenViewFactory.kt similarity index 100% rename from kotlin/samples/recyclerview/src/main/java/com/squareup/sample/recyclerview/BaseScreenViewFactory.kt rename to samples/recyclerview/src/main/java/com/squareup/sample/recyclerview/BaseScreenViewFactory.kt diff --git a/kotlin/samples/recyclerview/src/main/java/com/squareup/sample/recyclerview/ChooseRowTypeViewFactory.kt b/samples/recyclerview/src/main/java/com/squareup/sample/recyclerview/ChooseRowTypeViewFactory.kt similarity index 100% rename from kotlin/samples/recyclerview/src/main/java/com/squareup/sample/recyclerview/ChooseRowTypeViewFactory.kt rename to samples/recyclerview/src/main/java/com/squareup/sample/recyclerview/ChooseRowTypeViewFactory.kt diff --git a/kotlin/samples/recyclerview/src/main/java/com/squareup/sample/recyclerview/EditableListActivity.kt b/samples/recyclerview/src/main/java/com/squareup/sample/recyclerview/EditableListActivity.kt similarity index 100% rename from kotlin/samples/recyclerview/src/main/java/com/squareup/sample/recyclerview/EditableListActivity.kt rename to samples/recyclerview/src/main/java/com/squareup/sample/recyclerview/EditableListActivity.kt diff --git a/kotlin/samples/recyclerview/src/main/java/com/squareup/sample/recyclerview/editablelistworkflow/EditableListAdapter.kt b/samples/recyclerview/src/main/java/com/squareup/sample/recyclerview/editablelistworkflow/EditableListAdapter.kt similarity index 100% rename from kotlin/samples/recyclerview/src/main/java/com/squareup/sample/recyclerview/editablelistworkflow/EditableListAdapter.kt rename to samples/recyclerview/src/main/java/com/squareup/sample/recyclerview/editablelistworkflow/EditableListAdapter.kt diff --git a/kotlin/samples/recyclerview/src/main/java/com/squareup/sample/recyclerview/editablelistworkflow/EditableListLayoutRunner.kt b/samples/recyclerview/src/main/java/com/squareup/sample/recyclerview/editablelistworkflow/EditableListLayoutRunner.kt similarity index 100% rename from kotlin/samples/recyclerview/src/main/java/com/squareup/sample/recyclerview/editablelistworkflow/EditableListLayoutRunner.kt rename to samples/recyclerview/src/main/java/com/squareup/sample/recyclerview/editablelistworkflow/EditableListLayoutRunner.kt diff --git a/kotlin/samples/recyclerview/src/main/java/com/squareup/sample/recyclerview/editablelistworkflow/EditableListWorkflow.kt b/samples/recyclerview/src/main/java/com/squareup/sample/recyclerview/editablelistworkflow/EditableListWorkflow.kt similarity index 100% rename from kotlin/samples/recyclerview/src/main/java/com/squareup/sample/recyclerview/editablelistworkflow/EditableListWorkflow.kt rename to samples/recyclerview/src/main/java/com/squareup/sample/recyclerview/editablelistworkflow/EditableListWorkflow.kt diff --git a/kotlin/samples/recyclerview/src/main/java/com/squareup/sample/recyclerview/editablelistworkflow/ListDiffMode.kt b/samples/recyclerview/src/main/java/com/squareup/sample/recyclerview/editablelistworkflow/ListDiffMode.kt similarity index 100% rename from kotlin/samples/recyclerview/src/main/java/com/squareup/sample/recyclerview/editablelistworkflow/ListDiffMode.kt rename to samples/recyclerview/src/main/java/com/squareup/sample/recyclerview/editablelistworkflow/ListDiffMode.kt diff --git a/kotlin/samples/recyclerview/src/main/java/com/squareup/sample/recyclerview/editablelistworkflow/RowValue.kt b/samples/recyclerview/src/main/java/com/squareup/sample/recyclerview/editablelistworkflow/RowValue.kt similarity index 100% rename from kotlin/samples/recyclerview/src/main/java/com/squareup/sample/recyclerview/editablelistworkflow/RowValue.kt rename to samples/recyclerview/src/main/java/com/squareup/sample/recyclerview/editablelistworkflow/RowValue.kt diff --git a/kotlin/samples/recyclerview/src/main/java/com/squareup/sample/recyclerview/inputrows/CheckInputRow.kt b/samples/recyclerview/src/main/java/com/squareup/sample/recyclerview/inputrows/CheckInputRow.kt similarity index 100% rename from kotlin/samples/recyclerview/src/main/java/com/squareup/sample/recyclerview/inputrows/CheckInputRow.kt rename to samples/recyclerview/src/main/java/com/squareup/sample/recyclerview/inputrows/CheckInputRow.kt diff --git a/kotlin/samples/recyclerview/src/main/java/com/squareup/sample/recyclerview/inputrows/DropdownInputRow.kt b/samples/recyclerview/src/main/java/com/squareup/sample/recyclerview/inputrows/DropdownInputRow.kt similarity index 100% rename from kotlin/samples/recyclerview/src/main/java/com/squareup/sample/recyclerview/inputrows/DropdownInputRow.kt rename to samples/recyclerview/src/main/java/com/squareup/sample/recyclerview/inputrows/DropdownInputRow.kt diff --git a/kotlin/samples/recyclerview/src/main/java/com/squareup/sample/recyclerview/inputrows/InputRow.kt b/samples/recyclerview/src/main/java/com/squareup/sample/recyclerview/inputrows/InputRow.kt similarity index 100% rename from kotlin/samples/recyclerview/src/main/java/com/squareup/sample/recyclerview/inputrows/InputRow.kt rename to samples/recyclerview/src/main/java/com/squareup/sample/recyclerview/inputrows/InputRow.kt diff --git a/kotlin/samples/recyclerview/src/main/java/com/squareup/sample/recyclerview/inputrows/SwitchInputRow.kt b/samples/recyclerview/src/main/java/com/squareup/sample/recyclerview/inputrows/SwitchInputRow.kt similarity index 100% rename from kotlin/samples/recyclerview/src/main/java/com/squareup/sample/recyclerview/inputrows/SwitchInputRow.kt rename to samples/recyclerview/src/main/java/com/squareup/sample/recyclerview/inputrows/SwitchInputRow.kt diff --git a/kotlin/samples/recyclerview/src/main/java/com/squareup/sample/recyclerview/inputrows/TextInputRow.kt b/samples/recyclerview/src/main/java/com/squareup/sample/recyclerview/inputrows/TextInputRow.kt similarity index 100% rename from kotlin/samples/recyclerview/src/main/java/com/squareup/sample/recyclerview/inputrows/TextInputRow.kt rename to samples/recyclerview/src/main/java/com/squareup/sample/recyclerview/inputrows/TextInputRow.kt diff --git a/kotlin/samples/recyclerview/src/main/res/layout/base_screen_layout.xml b/samples/recyclerview/src/main/res/layout/base_screen_layout.xml similarity index 100% rename from kotlin/samples/recyclerview/src/main/res/layout/base_screen_layout.xml rename to samples/recyclerview/src/main/res/layout/base_screen_layout.xml diff --git a/kotlin/samples/recyclerview/src/main/res/layout/check_item.xml b/samples/recyclerview/src/main/res/layout/check_item.xml similarity index 98% rename from kotlin/samples/recyclerview/src/main/res/layout/check_item.xml rename to samples/recyclerview/src/main/res/layout/check_item.xml index c38af97b0..70dc0fa42 100644 --- a/kotlin/samples/recyclerview/src/main/res/layout/check_item.xml +++ b/samples/recyclerview/src/main/res/layout/check_item.xml @@ -38,4 +38,4 @@ android:text="@string/check_text" /> - \ No newline at end of file + diff --git a/kotlin/samples/recyclerview/src/main/res/layout/dropdown_item.xml b/samples/recyclerview/src/main/res/layout/dropdown_item.xml similarity index 98% rename from kotlin/samples/recyclerview/src/main/res/layout/dropdown_item.xml rename to samples/recyclerview/src/main/res/layout/dropdown_item.xml index 7a29c2fdc..e28158684 100644 --- a/kotlin/samples/recyclerview/src/main/res/layout/dropdown_item.xml +++ b/samples/recyclerview/src/main/res/layout/dropdown_item.xml @@ -37,4 +37,4 @@ android:layout_weight="1" /> - \ No newline at end of file + diff --git a/kotlin/samples/recyclerview/src/main/res/layout/new_row_type_item.xml b/samples/recyclerview/src/main/res/layout/new_row_type_item.xml similarity index 100% rename from kotlin/samples/recyclerview/src/main/res/layout/new_row_type_item.xml rename to samples/recyclerview/src/main/res/layout/new_row_type_item.xml diff --git a/kotlin/samples/recyclerview/src/main/res/layout/recyclerview_layout.xml b/samples/recyclerview/src/main/res/layout/recyclerview_layout.xml similarity index 100% rename from kotlin/samples/recyclerview/src/main/res/layout/recyclerview_layout.xml rename to samples/recyclerview/src/main/res/layout/recyclerview_layout.xml diff --git a/kotlin/samples/recyclerview/src/main/res/layout/switch_item.xml b/samples/recyclerview/src/main/res/layout/switch_item.xml similarity index 98% rename from kotlin/samples/recyclerview/src/main/res/layout/switch_item.xml rename to samples/recyclerview/src/main/res/layout/switch_item.xml index d19f73909..1b1c105a3 100644 --- a/kotlin/samples/recyclerview/src/main/res/layout/switch_item.xml +++ b/samples/recyclerview/src/main/res/layout/switch_item.xml @@ -38,4 +38,4 @@ android:text="@string/switch_text" /> - \ No newline at end of file + diff --git a/kotlin/samples/recyclerview/src/main/res/layout/text_item.xml b/samples/recyclerview/src/main/res/layout/text_item.xml similarity index 98% rename from kotlin/samples/recyclerview/src/main/res/layout/text_item.xml rename to samples/recyclerview/src/main/res/layout/text_item.xml index be7254c44..70f377728 100644 --- a/kotlin/samples/recyclerview/src/main/res/layout/text_item.xml +++ b/samples/recyclerview/src/main/res/layout/text_item.xml @@ -40,4 +40,4 @@ android:inputType="text" /> - \ No newline at end of file + diff --git a/kotlin/samples/recyclerview/src/main/res/values/dimens.xml b/samples/recyclerview/src/main/res/values/dimens.xml similarity index 98% rename from kotlin/samples/recyclerview/src/main/res/values/dimens.xml rename to samples/recyclerview/src/main/res/values/dimens.xml index 9a2dece33..aeef731c3 100644 --- a/kotlin/samples/recyclerview/src/main/res/values/dimens.xml +++ b/samples/recyclerview/src/main/res/values/dimens.xml @@ -19,4 +19,4 @@ 4dp 2dp 16dp - \ No newline at end of file + diff --git a/kotlin/samples/recyclerview/src/main/res/values/strings.xml b/samples/recyclerview/src/main/res/values/strings.xml similarity index 100% rename from kotlin/samples/recyclerview/src/main/res/values/strings.xml rename to samples/recyclerview/src/main/res/values/strings.xml diff --git a/kotlin/samples/recyclerview/src/main/res/values/styles.xml b/samples/recyclerview/src/main/res/values/styles.xml similarity index 100% rename from kotlin/samples/recyclerview/src/main/res/values/styles.xml rename to samples/recyclerview/src/main/res/values/styles.xml diff --git a/kotlin/samples/tictactoe/app/build.gradle.kts b/samples/tictactoe/app/build.gradle.kts similarity index 100% rename from kotlin/samples/tictactoe/app/build.gradle.kts rename to samples/tictactoe/app/build.gradle.kts diff --git a/kotlin/samples/tictactoe/app/src/androidTest/java/com/squareup/sample/TicTacToeEspressoTest.kt b/samples/tictactoe/app/src/androidTest/java/com/squareup/sample/TicTacToeEspressoTest.kt similarity index 100% rename from kotlin/samples/tictactoe/app/src/androidTest/java/com/squareup/sample/TicTacToeEspressoTest.kt rename to samples/tictactoe/app/src/androidTest/java/com/squareup/sample/TicTacToeEspressoTest.kt diff --git a/kotlin/samples/tictactoe/app/src/main/AndroidManifest.xml b/samples/tictactoe/app/src/main/AndroidManifest.xml similarity index 100% rename from kotlin/samples/tictactoe/app/src/main/AndroidManifest.xml rename to samples/tictactoe/app/src/main/AndroidManifest.xml diff --git a/kotlin/samples/tictactoe/app/src/main/java/com/squareup/sample/authworkflow/AuthViewFactories.kt b/samples/tictactoe/app/src/main/java/com/squareup/sample/authworkflow/AuthViewFactories.kt similarity index 100% rename from kotlin/samples/tictactoe/app/src/main/java/com/squareup/sample/authworkflow/AuthViewFactories.kt rename to samples/tictactoe/app/src/main/java/com/squareup/sample/authworkflow/AuthViewFactories.kt diff --git a/kotlin/samples/tictactoe/app/src/main/java/com/squareup/sample/authworkflow/AuthorizingViewFactory.kt b/samples/tictactoe/app/src/main/java/com/squareup/sample/authworkflow/AuthorizingViewFactory.kt similarity index 100% rename from kotlin/samples/tictactoe/app/src/main/java/com/squareup/sample/authworkflow/AuthorizingViewFactory.kt rename to samples/tictactoe/app/src/main/java/com/squareup/sample/authworkflow/AuthorizingViewFactory.kt diff --git a/kotlin/samples/tictactoe/app/src/main/java/com/squareup/sample/authworkflow/LoginViewFactory.kt b/samples/tictactoe/app/src/main/java/com/squareup/sample/authworkflow/LoginViewFactory.kt similarity index 100% rename from kotlin/samples/tictactoe/app/src/main/java/com/squareup/sample/authworkflow/LoginViewFactory.kt rename to samples/tictactoe/app/src/main/java/com/squareup/sample/authworkflow/LoginViewFactory.kt diff --git a/kotlin/samples/tictactoe/app/src/main/java/com/squareup/sample/authworkflow/SecondFactorViewFactory.kt b/samples/tictactoe/app/src/main/java/com/squareup/sample/authworkflow/SecondFactorViewFactory.kt similarity index 100% rename from kotlin/samples/tictactoe/app/src/main/java/com/squareup/sample/authworkflow/SecondFactorViewFactory.kt rename to samples/tictactoe/app/src/main/java/com/squareup/sample/authworkflow/SecondFactorViewFactory.kt diff --git a/kotlin/samples/tictactoe/app/src/main/java/com/squareup/sample/gameworkflow/Boards.kt b/samples/tictactoe/app/src/main/java/com/squareup/sample/gameworkflow/Boards.kt similarity index 100% rename from kotlin/samples/tictactoe/app/src/main/java/com/squareup/sample/gameworkflow/Boards.kt rename to samples/tictactoe/app/src/main/java/com/squareup/sample/gameworkflow/Boards.kt diff --git a/kotlin/samples/tictactoe/app/src/main/java/com/squareup/sample/gameworkflow/GameOverLayoutRunner.kt b/samples/tictactoe/app/src/main/java/com/squareup/sample/gameworkflow/GameOverLayoutRunner.kt similarity index 100% rename from kotlin/samples/tictactoe/app/src/main/java/com/squareup/sample/gameworkflow/GameOverLayoutRunner.kt rename to samples/tictactoe/app/src/main/java/com/squareup/sample/gameworkflow/GameOverLayoutRunner.kt diff --git a/kotlin/samples/tictactoe/app/src/main/java/com/squareup/sample/gameworkflow/GamePlayViewFactory.kt b/samples/tictactoe/app/src/main/java/com/squareup/sample/gameworkflow/GamePlayViewFactory.kt similarity index 100% rename from kotlin/samples/tictactoe/app/src/main/java/com/squareup/sample/gameworkflow/GamePlayViewFactory.kt rename to samples/tictactoe/app/src/main/java/com/squareup/sample/gameworkflow/GamePlayViewFactory.kt diff --git a/kotlin/samples/tictactoe/app/src/main/java/com/squareup/sample/gameworkflow/NewGameViewFactory.kt b/samples/tictactoe/app/src/main/java/com/squareup/sample/gameworkflow/NewGameViewFactory.kt similarity index 100% rename from kotlin/samples/tictactoe/app/src/main/java/com/squareup/sample/gameworkflow/NewGameViewFactory.kt rename to samples/tictactoe/app/src/main/java/com/squareup/sample/gameworkflow/NewGameViewFactory.kt diff --git a/kotlin/samples/tictactoe/app/src/main/java/com/squareup/sample/gameworkflow/TicTacToeViewBindings.kt b/samples/tictactoe/app/src/main/java/com/squareup/sample/gameworkflow/TicTacToeViewBindings.kt similarity index 100% rename from kotlin/samples/tictactoe/app/src/main/java/com/squareup/sample/gameworkflow/TicTacToeViewBindings.kt rename to samples/tictactoe/app/src/main/java/com/squareup/sample/gameworkflow/TicTacToeViewBindings.kt diff --git a/kotlin/samples/tictactoe/app/src/main/java/com/squareup/sample/mainactivity/MainActivity.kt b/samples/tictactoe/app/src/main/java/com/squareup/sample/mainactivity/MainActivity.kt similarity index 100% rename from kotlin/samples/tictactoe/app/src/main/java/com/squareup/sample/mainactivity/MainActivity.kt rename to samples/tictactoe/app/src/main/java/com/squareup/sample/mainactivity/MainActivity.kt diff --git a/kotlin/samples/tictactoe/app/src/main/java/com/squareup/sample/mainactivity/MainComponent.kt b/samples/tictactoe/app/src/main/java/com/squareup/sample/mainactivity/MainComponent.kt similarity index 100% rename from kotlin/samples/tictactoe/app/src/main/java/com/squareup/sample/mainactivity/MainComponent.kt rename to samples/tictactoe/app/src/main/java/com/squareup/sample/mainactivity/MainComponent.kt diff --git a/kotlin/samples/tictactoe/app/src/main/res/layout-land/game_play_layout.xml b/samples/tictactoe/app/src/main/res/layout-land/game_play_layout.xml similarity index 100% rename from kotlin/samples/tictactoe/app/src/main/res/layout-land/game_play_layout.xml rename to samples/tictactoe/app/src/main/res/layout-land/game_play_layout.xml diff --git a/kotlin/samples/tictactoe/app/src/main/res/layout/authorizing_layout.xml b/samples/tictactoe/app/src/main/res/layout/authorizing_layout.xml similarity index 100% rename from kotlin/samples/tictactoe/app/src/main/res/layout/authorizing_layout.xml rename to samples/tictactoe/app/src/main/res/layout/authorizing_layout.xml diff --git a/kotlin/samples/tictactoe/app/src/main/res/layout/board.xml b/samples/tictactoe/app/src/main/res/layout/board.xml similarity index 100% rename from kotlin/samples/tictactoe/app/src/main/res/layout/board.xml rename to samples/tictactoe/app/src/main/res/layout/board.xml diff --git a/kotlin/samples/tictactoe/app/src/main/res/layout/game_play_layout.xml b/samples/tictactoe/app/src/main/res/layout/game_play_layout.xml similarity index 100% rename from kotlin/samples/tictactoe/app/src/main/res/layout/game_play_layout.xml rename to samples/tictactoe/app/src/main/res/layout/game_play_layout.xml diff --git a/kotlin/samples/tictactoe/app/src/main/res/layout/login_layout.xml b/samples/tictactoe/app/src/main/res/layout/login_layout.xml similarity index 100% rename from kotlin/samples/tictactoe/app/src/main/res/layout/login_layout.xml rename to samples/tictactoe/app/src/main/res/layout/login_layout.xml diff --git a/kotlin/samples/tictactoe/app/src/main/res/layout/logout_decorator_layout.xml b/samples/tictactoe/app/src/main/res/layout/logout_decorator_layout.xml similarity index 100% rename from kotlin/samples/tictactoe/app/src/main/res/layout/logout_decorator_layout.xml rename to samples/tictactoe/app/src/main/res/layout/logout_decorator_layout.xml diff --git a/kotlin/samples/tictactoe/app/src/main/res/layout/new_game_layout.xml b/samples/tictactoe/app/src/main/res/layout/new_game_layout.xml similarity index 100% rename from kotlin/samples/tictactoe/app/src/main/res/layout/new_game_layout.xml rename to samples/tictactoe/app/src/main/res/layout/new_game_layout.xml diff --git a/kotlin/samples/tictactoe/app/src/main/res/layout/second_factor_layout.xml b/samples/tictactoe/app/src/main/res/layout/second_factor_layout.xml similarity index 100% rename from kotlin/samples/tictactoe/app/src/main/res/layout/second_factor_layout.xml rename to samples/tictactoe/app/src/main/res/layout/second_factor_layout.xml diff --git a/kotlin/samples/tictactoe/app/src/main/res/values/colors.xml b/samples/tictactoe/app/src/main/res/values/colors.xml similarity index 100% rename from kotlin/samples/tictactoe/app/src/main/res/values/colors.xml rename to samples/tictactoe/app/src/main/res/values/colors.xml diff --git a/kotlin/samples/tictactoe/app/src/main/res/values/strings.xml b/samples/tictactoe/app/src/main/res/values/strings.xml similarity index 100% rename from kotlin/samples/tictactoe/app/src/main/res/values/strings.xml rename to samples/tictactoe/app/src/main/res/values/strings.xml diff --git a/kotlin/samples/tictactoe/app/src/main/res/values/styles.xml b/samples/tictactoe/app/src/main/res/values/styles.xml similarity index 100% rename from kotlin/samples/tictactoe/app/src/main/res/values/styles.xml rename to samples/tictactoe/app/src/main/res/values/styles.xml diff --git a/kotlin/samples/tictactoe/common/build.gradle.kts b/samples/tictactoe/common/build.gradle.kts similarity index 100% rename from kotlin/samples/tictactoe/common/build.gradle.kts rename to samples/tictactoe/common/build.gradle.kts diff --git a/kotlin/samples/tictactoe/common/src/main/java/com/squareup/sample/authworkflow/AuthService.kt b/samples/tictactoe/common/src/main/java/com/squareup/sample/authworkflow/AuthService.kt similarity index 100% rename from kotlin/samples/tictactoe/common/src/main/java/com/squareup/sample/authworkflow/AuthService.kt rename to samples/tictactoe/common/src/main/java/com/squareup/sample/authworkflow/AuthService.kt diff --git a/kotlin/samples/tictactoe/common/src/main/java/com/squareup/sample/authworkflow/AuthWorkflow.kt b/samples/tictactoe/common/src/main/java/com/squareup/sample/authworkflow/AuthWorkflow.kt similarity index 100% rename from kotlin/samples/tictactoe/common/src/main/java/com/squareup/sample/authworkflow/AuthWorkflow.kt rename to samples/tictactoe/common/src/main/java/com/squareup/sample/authworkflow/AuthWorkflow.kt diff --git a/kotlin/samples/tictactoe/common/src/main/java/com/squareup/sample/authworkflow/AuthorizingScreen.kt b/samples/tictactoe/common/src/main/java/com/squareup/sample/authworkflow/AuthorizingScreen.kt similarity index 100% rename from kotlin/samples/tictactoe/common/src/main/java/com/squareup/sample/authworkflow/AuthorizingScreen.kt rename to samples/tictactoe/common/src/main/java/com/squareup/sample/authworkflow/AuthorizingScreen.kt diff --git a/kotlin/samples/tictactoe/common/src/main/java/com/squareup/sample/authworkflow/LoginScreen.kt b/samples/tictactoe/common/src/main/java/com/squareup/sample/authworkflow/LoginScreen.kt similarity index 100% rename from kotlin/samples/tictactoe/common/src/main/java/com/squareup/sample/authworkflow/LoginScreen.kt rename to samples/tictactoe/common/src/main/java/com/squareup/sample/authworkflow/LoginScreen.kt diff --git a/kotlin/samples/tictactoe/common/src/main/java/com/squareup/sample/authworkflow/RealAuthService.kt b/samples/tictactoe/common/src/main/java/com/squareup/sample/authworkflow/RealAuthService.kt similarity index 100% rename from kotlin/samples/tictactoe/common/src/main/java/com/squareup/sample/authworkflow/RealAuthService.kt rename to samples/tictactoe/common/src/main/java/com/squareup/sample/authworkflow/RealAuthService.kt diff --git a/kotlin/samples/tictactoe/common/src/main/java/com/squareup/sample/authworkflow/SecondFactorScreen.kt b/samples/tictactoe/common/src/main/java/com/squareup/sample/authworkflow/SecondFactorScreen.kt similarity index 100% rename from kotlin/samples/tictactoe/common/src/main/java/com/squareup/sample/authworkflow/SecondFactorScreen.kt rename to samples/tictactoe/common/src/main/java/com/squareup/sample/authworkflow/SecondFactorScreen.kt diff --git a/kotlin/samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/Board.kt b/samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/Board.kt similarity index 100% rename from kotlin/samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/Board.kt rename to samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/Board.kt diff --git a/kotlin/samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/CompletedGame.kt b/samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/CompletedGame.kt similarity index 100% rename from kotlin/samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/CompletedGame.kt rename to samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/CompletedGame.kt diff --git a/kotlin/samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/GameLog.kt b/samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/GameLog.kt similarity index 100% rename from kotlin/samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/GameLog.kt rename to samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/GameLog.kt diff --git a/kotlin/samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/GameOverScreen.kt b/samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/GameOverScreen.kt similarity index 100% rename from kotlin/samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/GameOverScreen.kt rename to samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/GameOverScreen.kt diff --git a/kotlin/samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/GamePlayScreen.kt b/samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/GamePlayScreen.kt similarity index 100% rename from kotlin/samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/GamePlayScreen.kt rename to samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/GamePlayScreen.kt diff --git a/kotlin/samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/NewGameScreen.kt b/samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/NewGameScreen.kt similarity index 100% rename from kotlin/samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/NewGameScreen.kt rename to samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/NewGameScreen.kt diff --git a/kotlin/samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/Player.kt b/samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/Player.kt similarity index 100% rename from kotlin/samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/Player.kt rename to samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/Player.kt diff --git a/kotlin/samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/PlayerInfo.kt b/samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/PlayerInfo.kt similarity index 100% rename from kotlin/samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/PlayerInfo.kt rename to samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/PlayerInfo.kt diff --git a/kotlin/samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/RunGameState.kt b/samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/RunGameState.kt similarity index 100% rename from kotlin/samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/RunGameState.kt rename to samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/RunGameState.kt diff --git a/kotlin/samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/RunGameWorkflow.kt b/samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/RunGameWorkflow.kt similarity index 100% rename from kotlin/samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/RunGameWorkflow.kt rename to samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/RunGameWorkflow.kt diff --git a/kotlin/samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/TakeTurnsWorkflow.kt b/samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/TakeTurnsWorkflow.kt similarity index 100% rename from kotlin/samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/TakeTurnsWorkflow.kt rename to samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/TakeTurnsWorkflow.kt diff --git a/kotlin/samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/Turn.kt b/samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/Turn.kt similarity index 100% rename from kotlin/samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/Turn.kt rename to samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/Turn.kt diff --git a/kotlin/samples/tictactoe/common/src/main/java/com/squareup/sample/mainworkflow/MainState.kt b/samples/tictactoe/common/src/main/java/com/squareup/sample/mainworkflow/MainState.kt similarity index 100% rename from kotlin/samples/tictactoe/common/src/main/java/com/squareup/sample/mainworkflow/MainState.kt rename to samples/tictactoe/common/src/main/java/com/squareup/sample/mainworkflow/MainState.kt diff --git a/kotlin/samples/tictactoe/common/src/main/java/com/squareup/sample/mainworkflow/MainWorkflow.kt b/samples/tictactoe/common/src/main/java/com/squareup/sample/mainworkflow/MainWorkflow.kt similarity index 100% rename from kotlin/samples/tictactoe/common/src/main/java/com/squareup/sample/mainworkflow/MainWorkflow.kt rename to samples/tictactoe/common/src/main/java/com/squareup/sample/mainworkflow/MainWorkflow.kt diff --git a/kotlin/samples/tictactoe/common/src/test/java/com/squareup/sample/gameworkflow/PlayerInfoTest.kt b/samples/tictactoe/common/src/test/java/com/squareup/sample/gameworkflow/PlayerInfoTest.kt similarity index 100% rename from kotlin/samples/tictactoe/common/src/test/java/com/squareup/sample/gameworkflow/PlayerInfoTest.kt rename to samples/tictactoe/common/src/test/java/com/squareup/sample/gameworkflow/PlayerInfoTest.kt diff --git a/kotlin/samples/tictactoe/common/src/test/java/com/squareup/sample/gameworkflow/TakeTurnsWorkflowTest.kt b/samples/tictactoe/common/src/test/java/com/squareup/sample/gameworkflow/TakeTurnsWorkflowTest.kt similarity index 100% rename from kotlin/samples/tictactoe/common/src/test/java/com/squareup/sample/gameworkflow/TakeTurnsWorkflowTest.kt rename to samples/tictactoe/common/src/test/java/com/squareup/sample/gameworkflow/TakeTurnsWorkflowTest.kt diff --git a/kotlin/samples/tictactoe/common/src/test/java/com/squareup/sample/mainworkflow/MainWorkflowTest.kt b/samples/tictactoe/common/src/test/java/com/squareup/sample/mainworkflow/MainWorkflowTest.kt similarity index 100% rename from kotlin/samples/tictactoe/common/src/test/java/com/squareup/sample/mainworkflow/MainWorkflowTest.kt rename to samples/tictactoe/common/src/test/java/com/squareup/sample/mainworkflow/MainWorkflowTest.kt diff --git a/kotlin/samples/todo-android/app/build.gradle.kts b/samples/todo-android/app/build.gradle.kts similarity index 100% rename from kotlin/samples/todo-android/app/build.gradle.kts rename to samples/todo-android/app/build.gradle.kts diff --git a/kotlin/samples/todo-android/app/src/androidTest/java/com/squareup/sample/mainactivity/TodoAppTest.kt b/samples/todo-android/app/src/androidTest/java/com/squareup/sample/mainactivity/TodoAppTest.kt similarity index 100% rename from kotlin/samples/todo-android/app/src/androidTest/java/com/squareup/sample/mainactivity/TodoAppTest.kt rename to samples/todo-android/app/src/androidTest/java/com/squareup/sample/mainactivity/TodoAppTest.kt diff --git a/kotlin/samples/todo-android/app/src/main/AndroidManifest.xml b/samples/todo-android/app/src/main/AndroidManifest.xml similarity index 100% rename from kotlin/samples/todo-android/app/src/main/AndroidManifest.xml rename to samples/todo-android/app/src/main/AndroidManifest.xml diff --git a/kotlin/samples/todo-android/app/src/main/java/com/squareup/sample/mainactivity/ItemListView.kt b/samples/todo-android/app/src/main/java/com/squareup/sample/mainactivity/ItemListView.kt similarity index 100% rename from kotlin/samples/todo-android/app/src/main/java/com/squareup/sample/mainactivity/ItemListView.kt rename to samples/todo-android/app/src/main/java/com/squareup/sample/mainactivity/ItemListView.kt diff --git a/kotlin/samples/todo-android/app/src/main/java/com/squareup/sample/mainactivity/MainActivity.kt b/samples/todo-android/app/src/main/java/com/squareup/sample/mainactivity/MainActivity.kt similarity index 100% rename from kotlin/samples/todo-android/app/src/main/java/com/squareup/sample/mainactivity/MainActivity.kt rename to samples/todo-android/app/src/main/java/com/squareup/sample/mainactivity/MainActivity.kt diff --git a/kotlin/samples/todo-android/app/src/main/java/com/squareup/sample/mainactivity/TextListeners.kt b/samples/todo-android/app/src/main/java/com/squareup/sample/mainactivity/TextListeners.kt similarity index 100% rename from kotlin/samples/todo-android/app/src/main/java/com/squareup/sample/mainactivity/TextListeners.kt rename to samples/todo-android/app/src/main/java/com/squareup/sample/mainactivity/TextListeners.kt diff --git a/kotlin/samples/todo-android/app/src/main/java/com/squareup/sample/mainactivity/TodoEditorLayoutRunner.kt b/samples/todo-android/app/src/main/java/com/squareup/sample/mainactivity/TodoEditorLayoutRunner.kt similarity index 100% rename from kotlin/samples/todo-android/app/src/main/java/com/squareup/sample/mainactivity/TodoEditorLayoutRunner.kt rename to samples/todo-android/app/src/main/java/com/squareup/sample/mainactivity/TodoEditorLayoutRunner.kt diff --git a/kotlin/samples/todo-android/app/src/main/java/com/squareup/sample/mainactivity/TodoListsViewFactory.kt b/samples/todo-android/app/src/main/java/com/squareup/sample/mainactivity/TodoListsViewFactory.kt similarity index 100% rename from kotlin/samples/todo-android/app/src/main/java/com/squareup/sample/mainactivity/TodoListsViewFactory.kt rename to samples/todo-android/app/src/main/java/com/squareup/sample/mainactivity/TodoListsViewFactory.kt diff --git a/kotlin/samples/todo-android/app/src/main/res/drawable/ic_delete_item.xml b/samples/todo-android/app/src/main/res/drawable/ic_delete_item.xml similarity index 100% rename from kotlin/samples/todo-android/app/src/main/res/drawable/ic_delete_item.xml rename to samples/todo-android/app/src/main/res/drawable/ic_delete_item.xml diff --git a/kotlin/samples/todo-android/app/src/main/res/drawable/list_selector.xml b/samples/todo-android/app/src/main/res/drawable/list_selector.xml similarity index 100% rename from kotlin/samples/todo-android/app/src/main/res/drawable/list_selector.xml rename to samples/todo-android/app/src/main/res/drawable/list_selector.xml diff --git a/kotlin/samples/todo-android/app/src/main/res/layout/todo_editor_layout.xml b/samples/todo-android/app/src/main/res/layout/todo_editor_layout.xml similarity index 100% rename from kotlin/samples/todo-android/app/src/main/res/layout/todo_editor_layout.xml rename to samples/todo-android/app/src/main/res/layout/todo_editor_layout.xml diff --git a/kotlin/samples/todo-android/app/src/main/res/layout/todo_item_layout.xml b/samples/todo-android/app/src/main/res/layout/todo_item_layout.xml similarity index 100% rename from kotlin/samples/todo-android/app/src/main/res/layout/todo_item_layout.xml rename to samples/todo-android/app/src/main/res/layout/todo_item_layout.xml diff --git a/kotlin/samples/todo-android/app/src/main/res/layout/todo_lists_layout.xml b/samples/todo-android/app/src/main/res/layout/todo_lists_layout.xml similarity index 100% rename from kotlin/samples/todo-android/app/src/main/res/layout/todo_lists_layout.xml rename to samples/todo-android/app/src/main/res/layout/todo_lists_layout.xml diff --git a/kotlin/samples/todo-android/app/src/main/res/layout/todo_lists_selectable_row_layout.xml b/samples/todo-android/app/src/main/res/layout/todo_lists_selectable_row_layout.xml similarity index 100% rename from kotlin/samples/todo-android/app/src/main/res/layout/todo_lists_selectable_row_layout.xml rename to samples/todo-android/app/src/main/res/layout/todo_lists_selectable_row_layout.xml diff --git a/kotlin/samples/todo-android/app/src/main/res/layout/todo_lists_unselectable_row_layout.xml b/samples/todo-android/app/src/main/res/layout/todo_lists_unselectable_row_layout.xml similarity index 100% rename from kotlin/samples/todo-android/app/src/main/res/layout/todo_lists_unselectable_row_layout.xml rename to samples/todo-android/app/src/main/res/layout/todo_lists_unselectable_row_layout.xml diff --git a/kotlin/samples/todo-android/app/src/main/res/values/colors.xml b/samples/todo-android/app/src/main/res/values/colors.xml similarity index 100% rename from kotlin/samples/todo-android/app/src/main/res/values/colors.xml rename to samples/todo-android/app/src/main/res/values/colors.xml diff --git a/kotlin/samples/todo-android/app/src/main/res/values/ids.xml b/samples/todo-android/app/src/main/res/values/ids.xml similarity index 100% rename from kotlin/samples/todo-android/app/src/main/res/values/ids.xml rename to samples/todo-android/app/src/main/res/values/ids.xml diff --git a/kotlin/samples/todo-android/app/src/main/res/values/strings.xml b/samples/todo-android/app/src/main/res/values/strings.xml similarity index 100% rename from kotlin/samples/todo-android/app/src/main/res/values/strings.xml rename to samples/todo-android/app/src/main/res/values/strings.xml diff --git a/kotlin/samples/todo-android/app/src/main/res/values/styles.xml b/samples/todo-android/app/src/main/res/values/styles.xml similarity index 100% rename from kotlin/samples/todo-android/app/src/main/res/values/styles.xml rename to samples/todo-android/app/src/main/res/values/styles.xml diff --git a/kotlin/samples/todo-android/common/build.gradle.kts b/samples/todo-android/common/build.gradle.kts similarity index 100% rename from kotlin/samples/todo-android/common/build.gradle.kts rename to samples/todo-android/common/build.gradle.kts diff --git a/kotlin/samples/todo-android/common/src/main/java/com/squareup/sample/todo/TodoEditorWorkflow.kt b/samples/todo-android/common/src/main/java/com/squareup/sample/todo/TodoEditorWorkflow.kt similarity index 100% rename from kotlin/samples/todo-android/common/src/main/java/com/squareup/sample/todo/TodoEditorWorkflow.kt rename to samples/todo-android/common/src/main/java/com/squareup/sample/todo/TodoEditorWorkflow.kt diff --git a/kotlin/samples/todo-android/common/src/main/java/com/squareup/sample/todo/TodoListsAppWorkflow.kt b/samples/todo-android/common/src/main/java/com/squareup/sample/todo/TodoListsAppWorkflow.kt similarity index 100% rename from kotlin/samples/todo-android/common/src/main/java/com/squareup/sample/todo/TodoListsAppWorkflow.kt rename to samples/todo-android/common/src/main/java/com/squareup/sample/todo/TodoListsAppWorkflow.kt diff --git a/kotlin/samples/todo-android/common/src/main/java/com/squareup/sample/todo/TodoListsScreen.kt b/samples/todo-android/common/src/main/java/com/squareup/sample/todo/TodoListsScreen.kt similarity index 100% rename from kotlin/samples/todo-android/common/src/main/java/com/squareup/sample/todo/TodoListsScreen.kt rename to samples/todo-android/common/src/main/java/com/squareup/sample/todo/TodoListsScreen.kt diff --git a/kotlin/samples/todo-android/common/src/main/java/com/squareup/sample/todo/TodoListsWorkflow.kt b/samples/todo-android/common/src/main/java/com/squareup/sample/todo/TodoListsWorkflow.kt similarity index 100% rename from kotlin/samples/todo-android/common/src/main/java/com/squareup/sample/todo/TodoListsWorkflow.kt rename to samples/todo-android/common/src/main/java/com/squareup/sample/todo/TodoListsWorkflow.kt diff --git a/kotlin/settings.gradle.kts b/settings.gradle.kts similarity index 100% rename from kotlin/settings.gradle.kts rename to settings.gradle.kts diff --git a/swift/README.md b/swift/README.md deleted file mode 100644 index 3ccc2ed55..000000000 --- a/swift/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Workflow - -[Reactive application architecture](https://www.github.com/square/workflow) diff --git a/swift/Samples/AlertContainer/AlertContainer.podspec b/swift/Samples/AlertContainer/AlertContainer.podspec deleted file mode 100644 index 4885b12ca..000000000 --- a/swift/Samples/AlertContainer/AlertContainer.podspec +++ /dev/null @@ -1,20 +0,0 @@ -Pod::Spec.new do |s| - s.name = 'AlertContainer' - s.version = '1.0.0.LOCAL' - s.summary = 'See the README.' - s.homepage = 'https://www.github.com/square/workflow' - s.license = 'Apache License, Version 2.0' - s.author = 'Square' - s.source = { git: 'Not Published', tag: "podify/#{s.version}" } - - # 1.7 is needed for `swift_versions` support - s.cocoapods_version = '>= 1.7.0' - - s.swift_versions = ['5.0'] - s.ios.deployment_target = '10.0' - - s.source_files = 'Sources/**/*.swift' - - s.dependency 'WorkflowUI' - -end diff --git a/swift/Samples/AlertContainer/README.md b/swift/Samples/AlertContainer/README.md deleted file mode 100644 index b060f946d..000000000 --- a/swift/Samples/AlertContainer/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# AlertContainer - -Container to display alert screens - diff --git a/swift/Samples/AlertContainer/Sources/AlertContainerScreen.swift b/swift/Samples/AlertContainer/Sources/AlertContainerScreen.swift deleted file mode 100644 index dc84afe0d..000000000 --- a/swift/Samples/AlertContainer/Sources/AlertContainerScreen.swift +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 WorkflowUI - -/// An `AlertContainerScreen` displays a base screen with an optional alert over top of it. -public struct AlertContainerScreen: Screen { - /// The base screen to show underneath any visible alert. - public var baseScreen: BaseScreen - - /// The presented alert. - public var alert: Alert? - - public init(baseScreen: BaseScreen, alert: Alert? = nil) { - self.baseScreen = baseScreen - self.alert = alert - } - - public func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { - return AlertContainerViewController.description(for: self, environment: environment) - } -} - -public struct Alert { - public var title: String - public var message: String - public var actions: [AlertAction] - - public init(title: String, message: String, actions: [AlertAction]) { - self.title = title - self.message = message - self.actions = actions - } -} - -public struct AlertAction { - public var title: String - public var style: Style - public var handler: () -> Void - - public init(title: String, style: Style, handler: @escaping () -> Void) { - self.title = title - self.style = style - self.handler = handler - } -} - -extension AlertAction { - public enum Style { - case `default` - case cancel - case destructive - } -} diff --git a/swift/Samples/AlertContainer/Sources/AlertContainerViewController.swift b/swift/Samples/AlertContainer/Sources/AlertContainerViewController.swift deleted file mode 100644 index 1a6011787..000000000 --- a/swift/Samples/AlertContainer/Sources/AlertContainerViewController.swift +++ /dev/null @@ -1,263 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 UIKit -import Workflow -import WorkflowUI - -private struct AlertStyleConstants { - static let viewWidth: CGFloat = 343.0 - static let buttonTitleColor = UIColor(red: 41 / 255, green: 150 / 255, blue: 204 / 255, alpha: 1.0) - static let titleFont = UIFont.systemFont(ofSize: 18, weight: .medium) -} - -internal final class AlertContainerViewController: ScreenViewController> { - private var baseScreenViewController: DescribedViewController - - private let dimmingView = UIView() - - private var alertView: AlertView? - - required init(screen: AlertContainerScreen, environment: ViewEnvironment) { - self.baseScreenViewController = DescribedViewController(screen: screen.baseScreen, environment: environment) - super.init(screen: screen, environment: environment) - } - - override func viewDidLoad() { - super.viewDidLoad() - - addChild(baseScreenViewController) - view.addSubview(baseScreenViewController.view) - baseScreenViewController.didMove(toParent: self) - - dimmingView.backgroundColor = UIColor(white: 0, alpha: 0.5) - view.addSubview(dimmingView) - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - - baseScreenViewController.view.frame = view.bounds - - dimmingView.frame = view.bounds - dimmingView.isUserInteractionEnabled = (alertView != nil) - dimmingView.alpha = (alertView != nil) ? 1 : 0 - } - - override func screenDidChange(from previousScreen: AlertContainerScreen, previousEnvironment: ViewEnvironment) { - super.screenDidChange(from: previousScreen, previousEnvironment: previousEnvironment) - - baseScreenViewController.update(screen: screen.baseScreen, environment: environment) - - if let alert = screen.alert { - if let alertView = alertView { - alertView.alert = alert - } else { - let inAlertView = AlertView(alert: alert) - inAlertView.backgroundColor = .init(white: 0.95, alpha: 1) - inAlertView.layer.cornerRadius = 10 - inAlertView.translatesAutoresizingMaskIntoConstraints = false - alertView = inAlertView - inAlertView.accessibilityViewIsModal = true - view.insertSubview(inAlertView, aboveSubview: dimmingView) - - NSLayoutConstraint.activate([ - inAlertView.centerXAnchor.constraint(equalTo: view.centerXAnchor), - inAlertView.centerYAnchor.constraint(equalTo: view.centerYAnchor), - inAlertView.heightAnchor.constraint(greaterThanOrEqualToConstant: 0), - inAlertView.widthAnchor.constraint(greaterThanOrEqualToConstant: AlertStyleConstants.viewWidth), - ]) - - view.setNeedsLayout() - view.layoutIfNeeded() - - dimmingView.alpha = 0 - - UIView.animate( - withDuration: 0.1, - delay: 0, - options: [ - .curveEaseInOut, - .allowUserInteraction, - ], - animations: { - self.dimmingView.alpha = 1 - inAlertView.transform = .identity - inAlertView.alpha = 1 - }, - completion: { _ in - UIAccessibility.post(notification: .screenChanged, argument: nil) - } - ) - } - } else { - if let alertView = alertView { - UIView.animate( - withDuration: 0.1, - delay: 0, - options: .curveEaseInOut, - animations: { - alertView.transform = CGAffineTransform(scaleX: 0.85, y: 0.85) - alertView.alpha = 0 - self.dimmingView.alpha = 0 - }, - completion: { _ in - alertView.removeFromSuperview() - self.view.setNeedsLayout() - UIAccessibility.post(notification: .screenChanged, argument: nil) - } - ) - self.alertView = nil - } - } - } - - override var childForStatusBarStyle: UIViewController? { - return baseScreenViewController - } - - override var childForStatusBarHidden: UIViewController? { - return baseScreenViewController - } - - override var childForHomeIndicatorAutoHidden: UIViewController? { - return baseScreenViewController - } - - override var childForScreenEdgesDeferringSystemGestures: UIViewController? { - return baseScreenViewController - } - - override public var supportedInterfaceOrientations: UIInterfaceOrientationMask { - return baseScreenViewController.supportedInterfaceOrientations - } -} - -private final class AlertView: UIView { - public var alert: Alert? - private lazy var title: UILabel = { - let title = UILabel() - title.font = AlertStyleConstants.titleFont - title.textAlignment = .center - title.translatesAutoresizingMaskIntoConstraints = false - return title - }() - - private lazy var message: UILabel = { - let message = UILabel() - message.font = AlertStyleConstants.titleFont - message.textAlignment = .center - message.numberOfLines = 0 - message.lineBreakMode = .byWordWrapping - message.translatesAutoresizingMaskIntoConstraints = false - return message - }() - - public required init(alert: Alert?) { - self.alert = alert - super.init(frame: .zero) - commonInit() - } - - private func commonInit() { - guard let alert = alert else { - return - } - title.text = alert.title - addSubview(title) - - message.text = alert.message - addSubview(message) - - let buttonStackView = setupButtons(actions: alert.actions) - addSubview(buttonStackView) - - var constraints: [NSLayoutConstraint] = [] - - constraints.append(title.topAnchor.constraint(equalTo: topAnchor, constant: 10)) - constraints.append(title.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -10)) - constraints.append(title.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 10)) - constraints.append(title.heightAnchor.constraint(equalToConstant: 25)) - - constraints.append(message.topAnchor.constraint(equalTo: title.bottomAnchor, constant: 10)) - constraints.append(message.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -10)) - constraints.append(message.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 10)) - - constraints.append(buttonStackView.topAnchor.constraint(equalTo: message.bottomAnchor, constant: 15)) - constraints.append(buttonStackView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 0)) - constraints.append(buttonStackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0)) - constraints.append(buttonStackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0)) - constraints.append(buttonStackView.heightAnchor.constraint(greaterThanOrEqualToConstant: 50)) - - addConstraints(constraints) - } - - private func setupButtons(actions: [AlertAction]) -> UIStackView { - let buttonStackView = UIStackView() - buttonStackView.axis = actions.count == 2 ? .horizontal : .vertical - buttonStackView.distribution = .fillEqually - buttonStackView.alignment = .fill - buttonStackView.translatesAutoresizingMaskIntoConstraints = false - - for action in actions { - let alertButton = AlertButton(action: action) - alertButton.backgroundColor = backgroundColor - alertButton.layer.borderColor = UIColor.gray.cgColor - alertButton.layer.borderWidth = 0.2 - alertButton.translatesAutoresizingMaskIntoConstraints = false - - buttonStackView.addArrangedSubview(alertButton) - } - - return buttonStackView - } - - @available(*, unavailable) - public required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} - -private final class AlertButton: UIButton { - private var action: AlertAction - - required init(action: AlertAction) { - self.action = action - super.init(frame: .zero) - commonInit() - } - - private func commonInit() { - setTitle(action.title, for: .normal) - - switch action.style { - case .default, .cancel: - setTitleColor(AlertStyleConstants.buttonTitleColor, for: .normal) - case .destructive: - setTitleColor(.systemRed, for: .normal) - } - - addTarget(self, action: #selector(triggerActionHandler), for: .touchUpInside) - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - @objc func triggerActionHandler() { - action.handler() - } -} diff --git a/swift/Samples/BackStackContainer/BackStackContainer.podspec b/swift/Samples/BackStackContainer/BackStackContainer.podspec deleted file mode 100644 index b27ad4140..000000000 --- a/swift/Samples/BackStackContainer/BackStackContainer.podspec +++ /dev/null @@ -1,20 +0,0 @@ -Pod::Spec.new do |s| - s.name = 'BackStackContainer' - s.version = '1.0.0.LOCAL' - s.summary = 'See the README.' - s.homepage = 'https://github.com/square/workflow' - s.license = 'Apache License, Version 2.0' - s.author = 'Square' - s.source = { git: 'Not Published', tag: "podify/#{s.version}" } - - # 1.7 is needed for `swift_versions` support - s.cocoapods_version = '>= 1.7.0' - - s.swift_versions = ['5.0'] - s.ios.deployment_target = '10.0' - - s.source_files = 'Sources/**/*.{swift}' - - s.dependency 'WorkflowUI' - -end diff --git a/swift/Samples/BackStackContainer/README.md b/swift/Samples/BackStackContainer/README.md deleted file mode 100644 index a301b43ab..000000000 --- a/swift/Samples/BackStackContainer/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# BackStackContainer - -An example of how a back stack container could be implemented allowing for declarative navigation backed by a UINavigationController. - -Given a list of `BackStackScreen.Item`s will update the navigation controller with all of the view controllers in the stack. The push and pop animations are based on if the changed list of back stack items contains a new or previous screen. diff --git a/swift/Samples/BackStackContainer/Sources/BackStackContainer.swift b/swift/Samples/BackStackContainer/Sources/BackStackContainer.swift deleted file mode 100644 index 15b82d6c2..000000000 --- a/swift/Samples/BackStackContainer/Sources/BackStackContainer.swift +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 WorkflowUI - -public final class BackStackContainer: ScreenViewController>, UINavigationControllerDelegate { - private let navController = UINavigationController() - - override public func viewDidLoad() { - super.viewDidLoad() - - navController.delegate = self - addChild(navController) - view.addSubview(navController.view) - navController.didMove(toParent: self) - } - - override public func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - - navController.view.frame = view.bounds - } - - override public func screenDidChange(from previousScreen: BackStackScreen, previousEnvironment: ViewEnvironment) { - super.screenDidChange(from: previousScreen, previousEnvironment: previousEnvironment) - - var existingViewControllers: [ScreenWrapperViewController] = navController.viewControllers as! [ScreenWrapperViewController] - var updatedViewControllers: [ScreenWrapperViewController] = [] - - for item in screen.items { - if let idx = existingViewControllers.firstIndex(where: { viewController -> Bool in - viewController.matches(item: item) - }) { - let existingViewController = existingViewControllers.remove(at: idx) - existingViewController.update(item: item, environment: environment) - updatedViewControllers.append(existingViewController) - } else { - updatedViewControllers.append(ScreenWrapperViewController(item: item, environment: environment)) - } - } - - navController.setViewControllers(updatedViewControllers, animated: true) - } - - // MARK: - UINavigationControllerDelegate - - public func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) { - setNavigationBarVisibility(with: screen, animated: animated) - } - - // MARK: - Private Methods - - private func setNavigationBarVisibility(with screen: BackStackScreen, animated: Bool) { - guard let topScreen = screen.items.last else { - return - } - - let hidden: Bool - - switch topScreen.barVisibility { - case .hidden: - hidden = true - - case .visible: - hidden = false - } - navController.setNavigationBarHidden(hidden, animated: animated) - } -} diff --git a/swift/Samples/BackStackContainer/Sources/BackStackScreen.swift b/swift/Samples/BackStackContainer/Sources/BackStackScreen.swift deleted file mode 100644 index be60f2231..000000000 --- a/swift/Samples/BackStackContainer/Sources/BackStackScreen.swift +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 WorkflowUI - -public struct BackStackScreen: Screen { - var items: [Item] - - public init(items: [Item]) { - self.items = items - } - - public func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { - return BackStackContainer.description(for: self, environment: environment) - } -} - -extension BackStackScreen { - /// A specific item in the back stack. The key and screen type is used to differentiate reused vs replaced screens. - public struct Item { - public var key: AnyHashable - public var screen: ScreenType - public var barVisibility: BarVisibility - - public init(key: Key?, screen: ScreenType, barVisibility: BarVisibility) { - self.screen = screen - - if let key = key { - self.key = AnyHashable(key) - } else { - self.key = AnyHashable(ObjectIdentifier(ScreenType.self)) - } - self.barVisibility = barVisibility - } - - public init(screen: ScreenType, barVisibility: BarVisibility) { - let key = Optional.none - self.init(key: key, screen: screen, barVisibility: barVisibility) - } - - public init(key: Key?, screen: ScreenType, barContent: BackStackScreen.BarContent) { - self.init(key: key, screen: screen, barVisibility: .visible(barContent)) - } - - public init(screen: ScreenType, barContent: BackStackScreen.BarContent) { - let key = Optional.none - self.init(key: key, screen: screen, barContent: barContent) - } - - public init(key: Key?, screen: ScreenType) { - let barVisibility: BarVisibility = .visible(BarContent()) - self.init(key: key, screen: screen, barVisibility: barVisibility) - } - - public init(screen: ScreenType) { - let key = Optional.none - self.init(key: key, screen: screen) - } - } -} - -extension BackStackScreen { - public enum BarVisibility { - case hidden - case visible(BarContent) - } -} - -extension BackStackScreen { - public struct BarContent { - var title: Title - var leftItem: BarButtonItem - var rightItem: BarButtonItem - - public enum BarButtonItem { - case none - case button(Button) - } - - public init(title: Title = .none, leftItem: BarButtonItem = .none, rightItem: BarButtonItem = .none) { - self.title = title - self.leftItem = leftItem - self.rightItem = rightItem - } - - public init(title: String, leftItem: BarButtonItem = .none, rightItem: BarButtonItem = .none) { - self.init(title: .text(title), leftItem: leftItem, rightItem: rightItem) - } - } -} - -extension BackStackScreen.BarContent { - public enum Title { - case none - case text(String) - } - - public enum ButtonContent { - case text(String) - case icon(UIImage) - } - - public struct Button { - var content: ButtonContent - var handler: () -> Void - - public init(content: ButtonContent, handler: @escaping () -> Void) { - self.content = content - self.handler = handler - } - - /// Convenience factory for a default back button. - public static func back(handler: @escaping () -> Void) -> Button { - return Button(content: .text("Back"), handler: handler) - } - } -} diff --git a/swift/Samples/BackStackContainer/Sources/ScreenWrapperViewController.swift b/swift/Samples/BackStackContainer/Sources/ScreenWrapperViewController.swift deleted file mode 100644 index 3d39225ec..000000000 --- a/swift/Samples/BackStackContainer/Sources/ScreenWrapperViewController.swift +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 WorkflowUI - -/** - Wrapper view controller for being hosted in a backstack. Handles updating the bar button items. - */ -final class ScreenWrapperViewController: UIViewController { - let key: AnyHashable - let environment: ViewEnvironment - - let contentViewController: DescribedViewController - - init(item: BackStackScreen.Item, environment: ViewEnvironment) { - self.key = item.key - self.environment = environment - self.contentViewController = DescribedViewController(screen: item.screen, environment: environment) - - super.init(nibName: nil, bundle: nil) - - update(barVisibility: item.barVisibility) - } - - override func viewDidLoad() { - super.viewDidLoad() - - view.backgroundColor = .white - - addChild(contentViewController) - view.addSubview(contentViewController.view) - contentViewController.didMove(toParent: self) - } - - override func viewWillLayoutSubviews() { - super.viewWillLayoutSubviews() - - contentViewController.view.frame = view.bounds - } - - func update(item: BackStackScreen.Item, environment: ViewEnvironment) { - contentViewController.update(screen: item.screen, environment: environment) - update(barVisibility: item.barVisibility) - } - - func matches(item: BackStackScreen.Item) -> Bool { - return item.key == key - && type(of: item.screen) == ScreenType.self - } - - private func update(barVisibility: BackStackScreen.BarVisibility) { - navigationItem.setHidesBackButton(true, animated: false) - - guard case let .visible(barContent) = barVisibility else { - return - } - - switch barContent.leftItem { - case .none: - if navigationItem.leftBarButtonItem != nil { - navigationItem.setLeftBarButton(nil, animated: true) - } - - case let .button(button): - if let leftItem = navigationItem.leftBarButtonItem as? CallbackBarButtonItem { - leftItem.update(with: button) - } else { - navigationItem.setLeftBarButton(CallbackBarButtonItem(button: button), animated: true) - } - } - - switch barContent.rightItem { - case .none: - if navigationItem.rightBarButtonItem != nil { - navigationItem.setRightBarButton(nil, animated: true) - } - - case let .button(button): - if let rightItem = navigationItem.rightBarButtonItem as? CallbackBarButtonItem { - rightItem.update(with: button) - } else { - navigationItem.setRightBarButton(CallbackBarButtonItem(button: button), animated: true) - } - } - - let title: String - switch barContent.title { - case .none: - title = "" - case let .text(text): - title = text - } - navigationItem.title = title - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} - -// MARK: - - -extension ScreenWrapperViewController { - final class CallbackBarButtonItem: UIBarButtonItem { - var handler: () -> Void - - init(button: BackStackScreen.BarContent.Button) { - self.handler = {} - - super.init() - self.target = self - self.action = #selector(onTapped) - update(with: button) - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func update(with button: BackStackScreen.BarContent.Button) { - switch button.content { - case let .text(title): - self.title = title - - case let .icon(image): - self.image = image - } - - handler = button.handler - } - - @objc private func onTapped() { - handler() - } - } -} diff --git a/swift/Samples/Dummy.swift b/swift/Samples/Dummy.swift deleted file mode 100644 index e69de29bb..000000000 diff --git a/swift/Samples/ModalContainer/ModalContainer.podspec b/swift/Samples/ModalContainer/ModalContainer.podspec deleted file mode 100644 index 6f563cdf8..000000000 --- a/swift/Samples/ModalContainer/ModalContainer.podspec +++ /dev/null @@ -1,20 +0,0 @@ -Pod::Spec.new do |s| - s.name = 'ModalContainer' - s.version = '1.0.0.LOCAL' - s.summary = 'See the README.' - s.homepage = 'https://www.github.com/square/workflow' - s.license = 'Apache License, Version 2.0' - s.author = 'Square' - s.source = { git: 'Not Published', tag: "podify/#{s.version}" } - - # 1.7 is needed for `swift_versions` support - s.cocoapods_version = '>= 1.7.0' - - s.swift_versions = ['5.0'] - s.ios.deployment_target = '10.0' - - s.source_files = 'Sources/**/*.swift' - - s.dependency 'WorkflowUI' - -end diff --git a/swift/Samples/ModalContainer/README.md b/swift/Samples/ModalContainer/README.md deleted file mode 100644 index 5afd7d1b0..000000000 --- a/swift/Samples/ModalContainer/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# ModalContainer - -Container to display screens modally diff --git a/swift/Samples/ModalContainer/Sources/ModalContainerScreen.swift b/swift/Samples/ModalContainer/Sources/ModalContainerScreen.swift deleted file mode 100644 index 22a7c5fee..000000000 --- a/swift/Samples/ModalContainer/Sources/ModalContainerScreen.swift +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 WorkflowUI - -/// A `ModalContainerScreen` displays a base screen and optionally one or more modals on top of it. -public struct ModalContainerScreen: Screen { - /// The base screen to show underneath any modally presented screens. - public let baseScreen: BaseScreen - - /// Modally presented screens - public let modals: [ModalContainerScreenModal] - - public init(baseScreen: BaseScreen, modals: [ModalContainerScreenModal]) { - self.baseScreen = baseScreen - self.modals = modals - } - - public func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { - return ModalContainerViewController.description(for: self, environment: environment) - } -} - -/// Represents a single screen to be displayed modally -public struct ModalContainerScreenModal { - public enum Style: Equatable { - // full screen modal presentation - case fullScreen - // formsheet or pagesheet like modal presentation - case sheet - } - - /// The screen to be displayed - public var screen: AnyScreen - - /// A bool used to specify whether presentation should be animated - public var animated: Bool - - /// The style in which the screen should be presented - public var style: Style - - /// A key used to differentiate modal screens during updates - public var key: AnyHashable - - public init(screen: AnyScreen, style: Style = .fullScreen, key: Key, animated: Bool = true) { - self.screen = screen - self.style = style - self.key = AnyHashable(key) - self.animated = animated - } -} diff --git a/swift/Samples/ModalContainer/Sources/ModalContainerViewController.swift b/swift/Samples/ModalContainer/Sources/ModalContainerViewController.swift deleted file mode 100644 index b1a215cf2..000000000 --- a/swift/Samples/ModalContainer/Sources/ModalContainerViewController.swift +++ /dev/null @@ -1,324 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 UIKit -import Workflow -import WorkflowUI - -/// Container for showing workflow screens modally over a base screen. -internal final class ModalContainerViewController: ScreenViewController> { - var baseScreenViewController: DescribedViewController - - private var presentedScreens: [ModallyPresentedScreen] = [] - - private var topmostScreenViewController: DescribedViewController? { - if let topModal = presentedScreens.last { - return topModal.viewController - } else { - return baseScreenViewController - } - } - - required init(screen: ModalContainerScreen, environment: ViewEnvironment) { - self.baseScreenViewController = DescribedViewController(screen: screen.baseScreen, environment: environment) - super.init(screen: screen, environment: environment) - } - - override func screenDidChange(from previousScreen: ModalContainerScreen, previousEnvironment: ViewEnvironment) { - super.screenDidChange(from: previousScreen, previousEnvironment: previousEnvironment) - - baseScreenViewController.update(screen: screen.baseScreen, environment: environment) - - // Sort our existing modals into keyed buckets. This will typically contain a single view controller - // per value, but duplicate keys/styles/screen types will result in more. In that case, we simply dequeue them in order during the update cycle (first to last) - var previousScreens: [ModalIdentifier: [ModallyPresentedScreen]] = Dictionary(presentedScreens.map { ($0.identifier, [$0]) }, uniquingKeysWith: +) - - // Will contain the new set of presented screens by the end of this method - var newScreens: [ModallyPresentedScreen] = [] - - var screensNeedingAppearanceTransition: [ModallyPresentedScreen] = [] - - for modal in screen.modals { - if let existing = previousScreens[modal.identifier]?.removeFirst() { - // Update existing screen view controller - existing.viewController.update(screen: modal.screen, environment: environment) - newScreens.append( - ModallyPresentedScreen( - viewController: existing.viewController, - style: modal.style, - key: modal.key, - dimmingView: existing.dimmingView, - animated: modal.animated - )) - } else { - // Make a new screen view controller - let newViewController = DescribedViewController(screen: modal.screen, environment: environment) - addChild(newViewController) - view.addSubview(newViewController.view) - newViewController.didMove(toParent: self) - - // Create and set a dimming view if the modal is in popover/formsheet or pagesheet style - var newDimmingView: UIView? - if modal.style == .sheet { - let dimmingView = UIView() - dimmingView.backgroundColor = UIColor(white: 0, alpha: 0.5) - dimmingView.frame = view.bounds - dimmingView.alpha = 0 - view.addSubview(dimmingView) - newDimmingView = dimmingView - } - - let modal = ModallyPresentedScreen( - viewController: newViewController, - style: modal.style, - key: modal.key, - dimmingView: newDimmingView, - animated: modal.animated - ) - newScreens.append(modal) - screensNeedingAppearanceTransition.append(modal) - } - } - - for modal in previousScreens.values.flatMap({ $0 }) { - // Anything left behind in `previousScreens` should be removed - - let displayInfo = ModalDisplayInfo(containerSize: view.bounds.size, style: modal.style, animated: modal.animated) - - modal.viewController.willMove(toParent: nil) - - UIView.animate( - withDuration: displayInfo.duration, - delay: 0.0, - options: displayInfo.animationOptions, - animations: { - modal.viewController.view.frame = displayInfo.outgoingFinalFrame - modal.viewController.view.transform = displayInfo.outgoingFinalTransform - modal.viewController.view.alpha = displayInfo.outgoingFinalAlpha - modal.dimmingView?.alpha = 0 - }, - completion: { _ in - modal.viewController.view.removeFromSuperview() - modal.viewController.removeFromParent() - modal.dimmingView?.removeFromSuperview() - } - ) - } - - for modal in screensNeedingAppearanceTransition { - let displayInfo = ModalDisplayInfo(containerSize: view.bounds.size, style: modal.style, animated: modal.animated) - modal.viewController.view.frame = displayInfo.incomingInitialFrame - modal.viewController.view.transform = displayInfo.incomingInitialTransform - modal.viewController.view.alpha = displayInfo.incomingInitialAlpha - - UIView.animate( - withDuration: displayInfo.duration, - delay: 0.0, - options: displayInfo.animationOptions, - animations: { - modal.viewController.view.bounds = CGRect( - origin: .zero, - size: displayInfo.frame.size - ) - modal.viewController.view.center = CGPoint( - x: displayInfo.frame.midX, - y: displayInfo.frame.midY - ) - modal.viewController.view.transform = displayInfo.transform - modal.viewController.view.alpha = displayInfo.alpha - modal.dimmingView?.alpha = 1 - }, - completion: { _ in - UIAccessibility.post(notification: .screenChanged, argument: nil) - } - ) - } - - // Update our state to reflect the new screens post-update - presentedScreens = newScreens - - // Sort our views. We go front to back to allow dismissed views to appear above currently presented views (this will matter after transition support is added). - for modal in presentedScreens.reversed() { - view.sendSubviewToBack(modal.viewController.view) - if let dimmingView = modal.dimmingView { - view.sendSubviewToBack(dimmingView) - } - } - - view.sendSubviewToBack(baseScreenViewController.view) - - setNeedsStatusBarAppearanceUpdate() - - if #available(iOS 11.0, *) { - setNeedsUpdateOfHomeIndicatorAutoHidden() - setNeedsUpdateOfScreenEdgesDeferringSystemGestures() - } - - // Set the topmost screen to be the accessibility modal - presentedScreens.last?.viewController.view.accessibilityViewIsModal = true - for modal in presentedScreens.dropLast() { - modal.viewController.view.accessibilityViewIsModal = false - } - } - - override func viewDidLoad() { - super.viewDidLoad() - - addChild(baseScreenViewController) - view.addSubview(baseScreenViewController.view) - baseScreenViewController.didMove(toParent: self) - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - baseScreenViewController.view.frame = view.bounds - - presentedScreens.forEach { - let displayInfo = ModalDisplayInfo(containerSize: view.bounds.size, style: $0.style, animated: $0.animated) - $0.viewController.view.frame = displayInfo.frame - $0.dimmingView?.frame = view.bounds - } - } - - override var childForStatusBarStyle: UIViewController? { - return topmostScreenViewController - } - - override var childForStatusBarHidden: UIViewController? { - return topmostScreenViewController - } - - override var childForHomeIndicatorAutoHidden: UIViewController? { - return topmostScreenViewController - } - - override var childForScreenEdgesDeferringSystemGestures: UIViewController? { - return topmostScreenViewController - } - - override public var supportedInterfaceOrientations: UIInterfaceOrientationMask { - return topmostScreenViewController?.supportedInterfaceOrientations ?? super.supportedInterfaceOrientations - } -} - -private struct ModallyPresentedScreen { - var viewController: DescribedViewController - var style: ModalContainerScreenModal.Style - var key: AnyHashable - var dimmingView: UIView? - var animated: Bool - - var identifier: ModalIdentifier { - return ModalIdentifier( - style: style, - key: key, - animated: animated - ) - } -} - -extension ModalContainerScreenModal { - fileprivate var identifier: ModalIdentifier { - return ModalIdentifier( - style: style, - key: key, - animated: animated - ) - } -} - -private struct ModalIdentifier: Hashable { - var style: ModalContainerScreenModal.Style - var key: AnyHashable - var animated: Bool -} - -private struct ModalDisplayInfo { - var frame: CGRect - var alpha: CGFloat - var transform: CGAffineTransform - var incomingInitialFrame: CGRect - var outgoingFinalFrame: CGRect - var incomingInitialTransform: CGAffineTransform - var outgoingFinalTransform: CGAffineTransform - var incomingInitialAlpha: CGFloat - var outgoingFinalAlpha: CGFloat - var duration: TimeInterval - var animationOptions: UIView.AnimationOptions - - init(containerSize: CGSize, style: ModalContainerScreenModal.Style, animated: Bool) { - // Configure all properties so that they default to fullScreen/sheet animation. - frame = CGRect(origin: .zero, size: containerSize) - alpha = 1.0 - transform = .identity - incomingInitialFrame = CGRect( - x: frame.origin.x, - y: containerSize.height, - width: frame.size.width, - height: frame.size.height - ) - outgoingFinalFrame = CGRect( - x: frame.origin.x, - y: containerSize.height, - width: frame.size.width, - height: frame.size.height - ) - incomingInitialTransform = .identity - outgoingFinalTransform = .identity - incomingInitialAlpha = 1.0 - outgoingFinalAlpha = 1.0 - duration = 0.5 - animationOptions = UIView.AnimationOptions(rawValue: 7 << 16) - - switch style { - case .fullScreen: - // Clear the default fullscreen animation configuration. - if !animated { - duration = 0 - animationOptions = UIView.AnimationOptions(rawValue: 0) - incomingInitialFrame = frame - outgoingFinalFrame = frame - } - case .sheet: - if UIDevice.current.userInterfaceIdiom == .phone { - // On iPhone always show modal in fullscreen. - break - } - - let popoverSideLength = min(containerSize.width, containerSize.height) - let popoverSize = CGSize(width: popoverSideLength, height: popoverSideLength) - let popOverOrigin = CGPoint( - x: (containerSize.width - popoverSideLength) / 2, - y: (containerSize.height - popoverSideLength) / 2 - ) - - frame = CGRect(origin: popOverOrigin, size: popoverSize) - - duration = 0.1 - animationOptions = UIView.AnimationOptions(rawValue: 0) - - // Do not animate frame. - incomingInitialFrame = frame - outgoingFinalFrame = frame - - // Animate transform and alpha. - incomingInitialTransform = CGAffineTransform(scaleX: 0.85, y: 0.85) - outgoingFinalTransform = CGAffineTransform(scaleX: 0.85, y: 0.85) - incomingInitialAlpha = 0.0 - outgoingFinalAlpha = 0.0 - } - } -} diff --git a/swift/Samples/SampleApp/.gitignore b/swift/Samples/SampleApp/.gitignore deleted file mode 100644 index 05ef11923..000000000 --- a/swift/Samples/SampleApp/.gitignore +++ /dev/null @@ -1 +0,0 @@ -Podfile.lock diff --git a/swift/Samples/SampleApp/Podfile b/swift/Samples/SampleApp/Podfile deleted file mode 100644 index 98d12662b..000000000 --- a/swift/Samples/SampleApp/Podfile +++ /dev/null @@ -1,13 +0,0 @@ -project 'SampleApp.xcodeproj' -platform :ios, '9.3' - -target 'SampleApp' do - pod 'Workflow', path: '../../../Workflow.podspec', :testspecs => ['Tests'] - pod 'WorkflowUI', path: '../../../WorkflowUI.podspec', :testspecs => ['Tests'] -end - -target 'SampleAppTests' do - pod 'Workflow', path: '../../../Workflow.podspec', :testspecs => ['Tests'] - pod 'WorkflowUI', path: '../../../WorkflowUI.podspec', :testspecs => ['Tests'] - pod 'WorkflowTesting', path: '../../../WorkflowTesting.podspec', :testspecs => ['Tests'] -end diff --git a/swift/Samples/SampleApp/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/swift/Samples/SampleApp/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index d8db8d65f..000000000 --- a/swift/Samples/SampleApp/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,98 +0,0 @@ -{ - "images" : [ - { - "idiom" : "iphone", - "size" : "20x20", - "scale" : "2x" - }, - { - "idiom" : "iphone", - "size" : "20x20", - "scale" : "3x" - }, - { - "idiom" : "iphone", - "size" : "29x29", - "scale" : "2x" - }, - { - "idiom" : "iphone", - "size" : "29x29", - "scale" : "3x" - }, - { - "idiom" : "iphone", - "size" : "40x40", - "scale" : "2x" - }, - { - "idiom" : "iphone", - "size" : "40x40", - "scale" : "3x" - }, - { - "idiom" : "iphone", - "size" : "60x60", - "scale" : "2x" - }, - { - "idiom" : "iphone", - "size" : "60x60", - "scale" : "3x" - }, - { - "idiom" : "ipad", - "size" : "20x20", - "scale" : "1x" - }, - { - "idiom" : "ipad", - "size" : "20x20", - "scale" : "2x" - }, - { - "idiom" : "ipad", - "size" : "29x29", - "scale" : "1x" - }, - { - "idiom" : "ipad", - "size" : "29x29", - "scale" : "2x" - }, - { - "idiom" : "ipad", - "size" : "40x40", - "scale" : "1x" - }, - { - "idiom" : "ipad", - "size" : "40x40", - "scale" : "2x" - }, - { - "idiom" : "ipad", - "size" : "76x76", - "scale" : "1x" - }, - { - "idiom" : "ipad", - "size" : "76x76", - "scale" : "2x" - }, - { - "idiom" : "ipad", - "size" : "83.5x83.5", - "scale" : "2x" - }, - { - "idiom" : "ios-marketing", - "size" : "1024x1024", - "scale" : "1x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/swift/Samples/SampleApp/Resources/Assets.xcassets/Contents.json b/swift/Samples/SampleApp/Resources/Assets.xcassets/Contents.json deleted file mode 100644 index da4a164c9..000000000 --- a/swift/Samples/SampleApp/Resources/Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/swift/Samples/SampleApp/Resources/Base.lproj/LaunchScreen.storyboard b/swift/Samples/SampleApp/Resources/Base.lproj/LaunchScreen.storyboard deleted file mode 100644 index bfa361294..000000000 --- a/swift/Samples/SampleApp/Resources/Base.lproj/LaunchScreen.storyboard +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/swift/Samples/SampleApp/Sources/AppDelegate.swift b/swift/Samples/SampleApp/Sources/AppDelegate.swift deleted file mode 100644 index a764cf0aa..000000000 --- a/swift/Samples/SampleApp/Sources/AppDelegate.swift +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 UIKit -import WorkflowUI - -@UIApplicationMain -class AppDelegate: UIResponder, UIApplicationDelegate { - var window: UIWindow? - - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - window = UIWindow(frame: UIScreen.main.bounds) - - window?.rootViewController = ContainerViewController(workflow: RootWorkflow()) - - window?.makeKeyAndVisible() - - return true - } -} diff --git a/swift/Samples/SampleApp/Sources/CrossFadeContainer.swift b/swift/Samples/SampleApp/Sources/CrossFadeContainer.swift deleted file mode 100644 index 0ce594507..000000000 --- a/swift/Samples/SampleApp/Sources/CrossFadeContainer.swift +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 Workflow -import WorkflowUI - -struct CrossFadeScreen: Screen { - var baseScreen: AnyScreen - var key: AnyHashable - - init(base screen: ScreenType, key: Key?) { - self.baseScreen = AnyScreen(screen) - if let key = key { - self.key = AnyHashable(key) - } else { - self.key = AnyHashable(ObjectIdentifier(ScreenType.self)) - } - } - - init(base screen: ScreenType) { - let key = Optional.none - self.init(base: screen, key: key) - } - - fileprivate func isEquivalent(to otherScreen: CrossFadeScreen) -> Bool { - return key == otherScreen.key - } - - func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { - return CrossFadeContainerViewController.description(for: self, environment: environment) - } -} - -private final class CrossFadeContainerViewController: ScreenViewController { - var childViewController: DescribedViewController - - required init(screen: CrossFadeScreen, environment: ViewEnvironment) { - self.childViewController = DescribedViewController(screen: screen.baseScreen, environment: environment) - super.init(screen: screen, environment: environment) - } - - override func viewDidLoad() { - super.viewDidLoad() - - addChild(childViewController) - view.addSubview(childViewController.view) - childViewController.didMove(toParent: self) - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - - childViewController.view.frame = view.bounds - } - - override func screenDidChange(from previousScreen: CrossFadeScreen, previousEnvironment: ViewEnvironment) { - super.screenDidChange(from: previousScreen, previousEnvironment: previousEnvironment) - - if screen.isEquivalent(to: previousScreen) { - childViewController.update(screen: screen.baseScreen, environment: environment) - } else { - // The new screen is different than the previous. Animate the transition. - let oldChild = childViewController - childViewController = DescribedViewController(screen: screen.baseScreen, environment: environment) - addChild(childViewController) - view.addSubview(childViewController.view) - UIView.transition( - from: oldChild.view, - to: childViewController.view, - duration: 0.72, - options: .transitionCrossDissolve, - completion: { [childViewController] completed in - childViewController.didMove(toParent: self) - - oldChild.willMove(toParent: nil) - oldChild.view.removeFromSuperview() - oldChild.removeFromParent() - } - ) - } - } -} diff --git a/swift/Samples/SampleApp/Sources/DemoScreen.swift b/swift/Samples/SampleApp/Sources/DemoScreen.swift deleted file mode 100644 index cdb6db074..000000000 --- a/swift/Samples/SampleApp/Sources/DemoScreen.swift +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 Workflow -import WorkflowUI - -struct DemoScreen: Screen { - let title: String - let color: UIColor - let onTitleTap: () -> Void - - let subscribeTitle: String - let onSubscribeTapped: () -> Void - - let refreshText: String - let isRefreshEnabled: Bool - let onRefreshTap: () -> Void - - func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { - return DemoViewController.description(for: self, environment: environment) - } -} - -private final class DemoViewController: ScreenViewController { - private let titleButton = UIButton(frame: .zero) - private let subscribeButton = UIButton(frame: .zero) - private let statusLabel = UILabel(frame: .zero) - private let refreshButton = UIButton(frame: .zero) - - override func viewDidLoad() { - super.viewDidLoad() - - titleButton.addTarget(self, action: #selector(titleButtonPressed(sender:)), for: .touchUpInside) - - subscribeButton.addTarget(self, action: #selector(subscribePressed(sender:)), for: .touchUpInside) - - statusLabel.textAlignment = .center - - refreshButton.addTarget(self, action: #selector(refreshButtonPressed(sender:)), for: .touchUpInside) - refreshButton.setTitle("Reverse!", for: .normal) - - view.addSubview(titleButton) - view.addSubview(subscribeButton) - view.addSubview(statusLabel) - view.addSubview(refreshButton) - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - - let height: CGFloat = 44.0 - let inset: CGFloat = 12.0 - - var (top, bottom) = view.bounds.divided(atDistance: view.bounds.height / 2, from: CGRectEdge.minYEdge) - - top.size.height -= (height / 2.0) - bottom.origin.y += height - bottom.size.height -= (height / 2.0) - - titleButton.frame = top - - subscribeButton.frame = CGRect( - x: 0.0, - y: top.maxY, - width: top.size.width, - height: height - ) - - let yOffset = bottom.midY - (height / 2.0) - - refreshButton.frame = CGRect( - x: bottom.origin.x, - y: yOffset, - width: bottom.size.width, - height: height - ) - .insetBy(dx: inset, dy: 0.0) - - statusLabel.frame = CGRect( - x: refreshButton.frame.origin.x, - y: yOffset - height, - width: refreshButton.frame.size.width, - height: height - ) - } - - override func screenDidChange(from previousScreen: DemoScreen, previousEnvironment: ViewEnvironment) { - super.screenDidChange(from: previousScreen, previousEnvironment: previousEnvironment) - - titleButton.setTitle(screen.title, for: .normal) - titleButton.backgroundColor = screen.color - - subscribeButton.setTitle(screen.subscribeTitle, for: .normal) - subscribeButton.backgroundColor = .black - - statusLabel.text = screen.refreshText - - refreshButton.isEnabled = screen.isRefreshEnabled - refreshButton.backgroundColor = UIColor( - red: 41 / 255, - green: 150 / 255, - blue: 204 / 255, - alpha: screen.isRefreshEnabled ? 1.0 : 0.5 - ) - } - - @objc private func titleButtonPressed(sender: UIButton) { - screen.onTitleTap() - } - - @objc private func subscribePressed(sender: UIButton) { - screen.onSubscribeTapped() - } - - @objc private func refreshButtonPressed(sender: UIButton) { - screen.onRefreshTap() - } -} diff --git a/swift/Samples/SampleApp/Sources/DemoWorkflow.swift b/swift/Samples/SampleApp/Sources/DemoWorkflow.swift deleted file mode 100644 index 31361323b..000000000 --- a/swift/Samples/SampleApp/Sources/DemoWorkflow.swift +++ /dev/null @@ -1,220 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 ReactiveSwift -import Workflow -import WorkflowUI - -// MARK: Input and Output - -struct DemoWorkflow: Workflow { - var name: String - - typealias Output = Never -} - -// MARK: State and Initialization - -extension DemoWorkflow { - struct State { - fileprivate var signal: TimerSignal - var colorState: ColorState - var loadingState: LoadingState - var subscriptionState: SubscriptionState - - enum ColorState { - case red - case green - case blue - } - - enum LoadingState { - case idle(title: String) - case loading - } - - enum SubscriptionState { - case not - case subscribing - } - } - - func makeInitialState() -> DemoWorkflow.State { - return State( - signal: TimerSignal(), - colorState: .red, - loadingState: .idle(title: "Not Loaded"), - subscriptionState: .not - ) - } -} - -// MARK: Actions - -extension DemoWorkflow { - enum Action: WorkflowAction { - typealias WorkflowType = DemoWorkflow - - case titleButtonTapped - case subscribeTapped - case refreshButtonTapped - case refreshComplete(String) - case refreshError(Error) - - func apply(toState state: inout DemoWorkflow.State) -> DemoWorkflow.Output? { - switch self { - case .titleButtonTapped: - switch state.colorState { - case .red: - state.colorState = .green - case .green: - state.colorState = .blue - case .blue: - state.colorState = .red - } - - case .subscribeTapped: - switch state.subscriptionState { - case .not: - state.subscriptionState = .subscribing - case .subscribing: - state.subscriptionState = .not - } - - case .refreshButtonTapped: - state.loadingState = .loading - case let .refreshComplete(message): - state.loadingState = .idle(title: message) - case let .refreshError(error): - state.loadingState = .idle(title: error.localizedDescription) - } - return nil - } - } -} - -// MARK: Workers - -struct RefreshWorker: Worker { - enum Output { - case success(String) - case error(Error) - } - - func run() -> SignalProducer { - return SignalProducer(value: .success("We did it!")) - .delay(1.0, on: QueueScheduler.main) - } - - func isEquivalent(to otherWorker: RefreshWorker) -> Bool { - return true - } -} - -// MARK: Rendering - -extension DemoWorkflow { - typealias Rendering = DemoScreen - - func render(state: DemoWorkflow.State, context: RenderContext) -> Rendering { - let color: UIColor - switch state.colorState { - case .red: - color = .red - case .green: - color = .green - case .blue: - color = .blue - } - - var title = "Hello, \(name)!" - let refreshText: String - let refreshEnabled: Bool - - switch state.loadingState { - case let .idle(title: refreshTitle): - refreshText = refreshTitle - refreshEnabled = true - - title = ReversingWorkflow(text: title) - .rendered(with: context) - - case .loading: - refreshText = "Loading..." - refreshEnabled = false - - context.awaitResult(for: RefreshWorker()) { output -> Action in - switch output { - case let .success(result): - return .refreshComplete(result) - case let .error(error): - return .refreshError(error) - } - } - } - - let subscribeTitle: String - - switch state.subscriptionState { - case .not: - subscribeTitle = "Subscribe" - case .subscribing: - // Subscribe to the timer signal, simulating the title being tapped. - context.awaitResult(for: state.signal.signal.asWorker(key: "Timer")) { _ -> Action in - .titleButtonTapped - } - subscribeTitle = "Stop" - } - - // Create a sink of our Action type so we can send actions back to the workflow. - let sink = context.makeSink(of: Action.self) - - return DemoScreen( - title: title, - color: color, - onTitleTap: { - sink.send(.titleButtonTapped) - }, - subscribeTitle: subscribeTitle, - onSubscribeTapped: { - sink.send(.subscribeTapped) - }, - refreshText: refreshText, - isRefreshEnabled: refreshEnabled, - onRefreshTap: { - sink.send(.refreshButtonTapped) - } - ) - } -} - -private class TimerSignal { - let signal: Signal - let observer: Signal.Observer - let timer: Timer - - init() { - let (signal, observer) = Signal.pipe() - - let timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak observer] _ in - observer?.send(value: ()) - } - - self.signal = signal - self.observer = observer - self.timer = timer - } -} diff --git a/swift/Samples/SampleApp/Sources/ReversingWorkflow.swift b/swift/Samples/SampleApp/Sources/ReversingWorkflow.swift deleted file mode 100644 index 754dfd63c..000000000 --- a/swift/Samples/SampleApp/Sources/ReversingWorkflow.swift +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 Workflow -import WorkflowUI - -// MARK: Input and Output - -/// This is a stateless workflow. It only used the properties sent from its parent to render a result. -struct ReversingWorkflow: Workflow { - typealias Rendering = String - typealias Output = Never - typealias State = Void - - var text: String -} - -// MARK: Rendering - -extension ReversingWorkflow { - func render(state: ReversingWorkflow.State, context: RenderContext) -> String { - return String(text.reversed()) - } -} diff --git a/swift/Samples/SampleApp/Sources/RootWorkflow.swift b/swift/Samples/SampleApp/Sources/RootWorkflow.swift deleted file mode 100644 index cae883166..000000000 --- a/swift/Samples/SampleApp/Sources/RootWorkflow.swift +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 ReactiveSwift -import Workflow -import WorkflowUI - -// MARK: Input and Output - -struct RootWorkflow: Workflow { - typealias Output = Never -} - -// MARK: State and Initialization - -extension RootWorkflow { - enum State { - case welcome - case demo(name: String) - } - - func makeInitialState() -> RootWorkflow.State { - return .welcome - } -} - -// MARK: Actions - -extension RootWorkflow { - enum Action: WorkflowAction { - typealias WorkflowType = RootWorkflow - - case login(name: String) - - func apply(toState state: inout RootWorkflow.State) -> RootWorkflow.Output? { - switch self { - case let .login(name: name): - state = .demo(name: name) - } - - return nil - } - } -} - -// MARK: Rendering - -extension RootWorkflow { - typealias Rendering = CrossFadeScreen - - func render(state: RootWorkflow.State, context: RenderContext) -> Rendering { - switch state { - case .welcome: - return CrossFadeScreen( - base: WelcomeWorkflow() - .mapOutput { output -> Action in - switch output { - case let .login(name: name): - return .login(name: name) - } - } - .rendered(with: context) - ) - - case let .demo(name: name): - return CrossFadeScreen( - base: DemoWorkflow(name: name) - .rendered(with: context) - ) - } - } -} diff --git a/swift/Samples/SampleApp/Sources/WelcomeScreen.swift b/swift/Samples/SampleApp/Sources/WelcomeScreen.swift deleted file mode 100644 index 4101eedb2..000000000 --- a/swift/Samples/SampleApp/Sources/WelcomeScreen.swift +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 Workflow -import WorkflowUI - -struct WelcomeScreen: Screen { - var name: String - var onNameChanged: (String) -> Void - var onLoginTapped: () -> Void - - func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { - return WelcomeViewController.description(for: self, environment: environment) - } -} - -private final class WelcomeViewController: ScreenViewController { - let welcomeLabel = UILabel(frame: .zero) - let nameField = UITextField(frame: .zero) - let button = UIButton(frame: .zero) - - override func viewDidLoad() { - super.viewDidLoad() - - welcomeLabel.text = "Welcome! Please Enter Your Name" - welcomeLabel.textAlignment = .center - - nameField.backgroundColor = UIColor(white: 0.92, alpha: 1.0) - nameField.addTarget(self, action: #selector(textDidChange(sender:)), for: .editingChanged) - - button.backgroundColor = UIColor(red: 41 / 255, green: 150 / 255, blue: 204 / 255, alpha: 1.0) - button.setTitle("Login", for: .normal) - button.addTarget(self, action: #selector(buttonTapped(sender:)), for: .touchUpInside) - - view.addSubview(welcomeLabel) - view.addSubview(nameField) - view.addSubview(button) - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - - let inset: CGFloat = 12.0 - let height: CGFloat = 44.0 - var yOffset = (view.bounds.size.height - (2 * height + inset)) / 2.0 - - welcomeLabel.frame = CGRect( - x: view.bounds.origin.x, - y: view.bounds.origin.y, - width: view.bounds.size.width, - height: yOffset - ) - - nameField.frame = CGRect( - x: view.bounds.origin.x, - y: yOffset, - width: view.bounds.size.width, - height: height - ) - .insetBy(dx: inset, dy: 0.0) - - yOffset += height + inset - button.frame = CGRect( - x: view.bounds.origin.x, - y: yOffset, - width: view.bounds.size.width, - height: height - ) - .insetBy(dx: inset, dy: 0.0) - } - - override func screenDidChange(from previousScreen: WelcomeScreen, previousEnvironment: ViewEnvironment) { - super.screenDidChange(from: previousScreen, previousEnvironment: previousEnvironment) - - nameField.text = screen.name - } - - @objc private func textDidChange(sender: UITextField) { - guard let text = sender.text else { - return - } - screen.onNameChanged(text) - } - - @objc private func buttonTapped(sender: UIButton) { - screen.onLoginTapped() - } -} diff --git a/swift/Samples/SampleApp/Sources/WelcomeWorkflow.swift b/swift/Samples/SampleApp/Sources/WelcomeWorkflow.swift deleted file mode 100644 index 68178b3b6..000000000 --- a/swift/Samples/SampleApp/Sources/WelcomeWorkflow.swift +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 Workflow -import WorkflowUI - -// MARK: Input and Output - -struct WelcomeWorkflow: Workflow { - enum Output { - case login(name: String) - } -} - -// MARK: State and Initialization - -extension WelcomeWorkflow { - struct State { - var name: String - } - - func makeInitialState() -> WelcomeWorkflow.State { - return State(name: "") - } -} - -// MARK: Actions - -extension WelcomeWorkflow { - enum Action: WorkflowAction { - typealias WorkflowType = WelcomeWorkflow - - case nameChanged(String) - case login - - func apply(toState state: inout WelcomeWorkflow.State) -> WelcomeWorkflow.Output? { - switch self { - case let .nameChanged(updatedName): - state.name = updatedName - return nil - - case .login: - return .login(name: state.name) - } - } - } -} - -// MARK: Rendering - -extension WelcomeWorkflow { - typealias Rendering = WelcomeScreen - - func render(state: WelcomeWorkflow.State, context: RenderContext) -> Rendering { - let sink = context.makeSink(of: Action.self) - return WelcomeScreen( - name: state.name, - onNameChanged: { updatedName in - sink.send(.nameChanged(updatedName)) - }, - onLoginTapped: { - sink.send(.login) - } - ) - } -} diff --git a/swift/Samples/SampleSwiftUIApp/.gitignore b/swift/Samples/SampleSwiftUIApp/.gitignore deleted file mode 100644 index 05ef11923..000000000 --- a/swift/Samples/SampleSwiftUIApp/.gitignore +++ /dev/null @@ -1 +0,0 @@ -Podfile.lock diff --git a/swift/Samples/SampleSwiftUIApp/Podfile b/swift/Samples/SampleSwiftUIApp/Podfile deleted file mode 100644 index 0e5ba2b94..000000000 --- a/swift/Samples/SampleSwiftUIApp/Podfile +++ /dev/null @@ -1,7 +0,0 @@ -project 'SampleSwiftUIApp.xcodeproj' -platform :ios, '13' - -target 'SampleSwiftUIApp' do - pod 'Workflow', path: '../../../Workflow.podspec', :testspecs => ['Tests'] - pod 'WorkflowSwiftUI', path: '../../../WorkflowSwiftUI.podspec' -end diff --git a/swift/Samples/SampleSwiftUIApp/SampleSwiftUIApp/AppDelegate.swift b/swift/Samples/SampleSwiftUIApp/SampleSwiftUIApp/AppDelegate.swift deleted file mode 100644 index e726b1aba..000000000 --- a/swift/Samples/SampleSwiftUIApp/SampleSwiftUIApp/AppDelegate.swift +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 UIKit - -@UIApplicationMain -class AppDelegate: UIResponder, UIApplicationDelegate { - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - // Override point for customization after application launch. - return true - } - - // MARK: UISceneSession Lifecycle - - func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { - // Called when a new scene session is being created. - // Use this method to select a configuration to create the new scene with. - return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) - } - - func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { - // Called when the user discards a scene session. - // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. - // Use this method to release any resources that were specific to the discarded scenes, as they will not return. - } -} diff --git a/swift/Samples/SampleSwiftUIApp/SampleSwiftUIApp/CounterView.swift b/swift/Samples/SampleSwiftUIApp/SampleSwiftUIApp/CounterView.swift deleted file mode 100644 index 559a5ab2e..000000000 --- a/swift/Samples/SampleSwiftUIApp/SampleSwiftUIApp/CounterView.swift +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 SwiftUI -import Workflow -import WorkflowSwiftUI - -struct CounterView: View { - var body: some View { - WorkflowView( - workflow: CounterWorkflow(), - onOutput: { _ in } - ) { rendering in - VStack { - Text("The value is \(rendering.value)") - Button(action: rendering.onIncrement) { - Text("+") - } - Button(action: rendering.onDecrement) { - Text("-") - } - } - } - } -} - -struct CounterScreen { - let value: Int - var onIncrement: () -> Void - var onDecrement: () -> Void -} - -struct CounterWorkflow: Workflow { - enum Action: WorkflowAction { - case increment - case decrement - - func apply(toState state: inout Int) -> Never? { - switch self { - case .increment: - state += 1 - case .decrement: - state -= 1 - } - return nil - } - - typealias WorkflowType = CounterWorkflow - } - - func makeInitialState() -> Int { - return 0 - } - - func workflowDidChange(from previousWorkflow: CounterWorkflow, state: inout Int) {} - - func render(state: Int, context: RenderContext) -> CounterScreen { - let sink = context.makeSink(of: Action.self) - return CounterScreen( - value: state, - onIncrement: { - sink.send(.increment) - }, - onDecrement: { - sink.send(.decrement) - } - ) - } - - typealias Output = Never -} - -struct ContentView_Previews: PreviewProvider { - static var previews: some View { - CounterView() - } -} diff --git a/swift/Samples/SampleSwiftUIApp/SampleSwiftUIApp/Info.plist b/swift/Samples/SampleSwiftUIApp/SampleSwiftUIApp/Info.plist deleted file mode 100644 index 41456fbdd..000000000 --- a/swift/Samples/SampleSwiftUIApp/SampleSwiftUIApp/Info.plist +++ /dev/null @@ -1,60 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - $(PRODUCT_BUNDLE_PACKAGE_TYPE) - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - LSRequiresIPhoneOS - - UIApplicationSceneManifest - - UIApplicationSupportsMultipleScenes - - UISceneConfigurations - - UIWindowSceneSessionRoleApplication - - - UISceneConfigurationName - Default Configuration - UISceneDelegateClassName - $(PRODUCT_MODULE_NAME).SceneDelegate - - - - - UILaunchStoryboardName - - UIRequiredDeviceCapabilities - - armv7 - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - - diff --git a/swift/Samples/SampleSwiftUIApp/SampleSwiftUIApp/SceneDelegate.swift b/swift/Samples/SampleSwiftUIApp/SampleSwiftUIApp/SceneDelegate.swift deleted file mode 100644 index 30162d3b0..000000000 --- a/swift/Samples/SampleSwiftUIApp/SampleSwiftUIApp/SceneDelegate.swift +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 SwiftUI -import UIKit - -class SceneDelegate: UIResponder, UIWindowSceneDelegate { - var window: UIWindow? - - func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { - // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. - // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. - // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). - - // Create the SwiftUI view that provides the window contents. - let contentView = CounterView() - - // Use a UIHostingController as window root view controller. - if let windowScene = scene as? UIWindowScene { - let window = UIWindow(windowScene: windowScene) - window.rootViewController = UIHostingController(rootView: contentView) - self.window = window - window.makeKeyAndVisible() - } - } - - func sceneDidDisconnect(_ scene: UIScene) { - // Called as the scene is being released by the system. - // This occurs shortly after the scene enters the background, or when its session is discarded. - // Release any resources associated with this scene that can be re-created the next time the scene connects. - // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). - } - - func sceneDidBecomeActive(_ scene: UIScene) { - // Called when the scene has moved from an inactive state to an active state. - // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. - } - - func sceneWillResignActive(_ scene: UIScene) { - // Called when the scene will move from an active state to an inactive state. - // This may occur due to temporary interruptions (ex. an incoming phone call). - } - - func sceneWillEnterForeground(_ scene: UIScene) { - // Called as the scene transitions from the background to the foreground. - // Use this method to undo the changes made on entering the background. - } - - func sceneDidEnterBackground(_ scene: UIScene) { - // Called as the scene transitions from the foreground to the background. - // Use this method to save data, release shared resources, and store enough scene-specific state information - // to restore the scene back to its current state. - } -} diff --git a/swift/Samples/SnapshotTests/ReferenceImages_64/SplitScreenContainerScreenSnapshotTests/test_splitRatio_custom_iPad.png b/swift/Samples/SnapshotTests/ReferenceImages_64/SplitScreenContainerScreenSnapshotTests/test_splitRatio_custom_iPad.png deleted file mode 100644 index c3bdf6e96..000000000 Binary files a/swift/Samples/SnapshotTests/ReferenceImages_64/SplitScreenContainerScreenSnapshotTests/test_splitRatio_custom_iPad.png and /dev/null differ diff --git a/swift/Samples/SnapshotTests/ReferenceImages_64/SplitScreenContainerScreenSnapshotTests/test_splitRatio_half_iPad.png b/swift/Samples/SnapshotTests/ReferenceImages_64/SplitScreenContainerScreenSnapshotTests/test_splitRatio_half_iPad.png deleted file mode 100644 index c937dad3e..000000000 Binary files a/swift/Samples/SnapshotTests/ReferenceImages_64/SplitScreenContainerScreenSnapshotTests/test_splitRatio_half_iPad.png and /dev/null differ diff --git a/swift/Samples/SnapshotTests/ReferenceImages_64/SplitScreenContainerScreenSnapshotTests/test_splitRatio_quarter_iPad.png b/swift/Samples/SnapshotTests/ReferenceImages_64/SplitScreenContainerScreenSnapshotTests/test_splitRatio_quarter_iPad.png deleted file mode 100644 index 43d960979..000000000 Binary files a/swift/Samples/SnapshotTests/ReferenceImages_64/SplitScreenContainerScreenSnapshotTests/test_splitRatio_quarter_iPad.png and /dev/null differ diff --git a/swift/Samples/SnapshotTests/ReferenceImages_64/SplitScreenContainerScreenSnapshotTests/test_splitRatio_third_iPad.png b/swift/Samples/SnapshotTests/ReferenceImages_64/SplitScreenContainerScreenSnapshotTests/test_splitRatio_third_iPad.png deleted file mode 100644 index 55da98c57..000000000 Binary files a/swift/Samples/SnapshotTests/ReferenceImages_64/SplitScreenContainerScreenSnapshotTests/test_splitRatio_third_iPad.png and /dev/null differ diff --git a/swift/Samples/SplitScreenContainer/DemoApp/AppDelegate.swift b/swift/Samples/SplitScreenContainer/DemoApp/AppDelegate.swift deleted file mode 100644 index dbe866f85..000000000 --- a/swift/Samples/SplitScreenContainer/DemoApp/AppDelegate.swift +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 SplitScreenContainer -import UIKit -import Workflow -import WorkflowUI - -@UIApplicationMain -class AppDelegate: UIResponder, UIApplicationDelegate { - var window: UIWindow? - - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - let window = UIWindow(frame: UIScreen.main.bounds) - - let container = ContainerViewController( - workflow: DemoWorkflow() - ) - - window.rootViewController = container - self.window = window - window.makeKeyAndVisible() - return true - } -} diff --git a/swift/Samples/SplitScreenContainer/DemoApp/BarScreen.swift b/swift/Samples/SplitScreenContainer/DemoApp/BarScreen.swift deleted file mode 100644 index 51f1061a2..000000000 --- a/swift/Samples/SplitScreenContainer/DemoApp/BarScreen.swift +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 Workflow -import WorkflowUI - -struct BarScreen: Screen { - let title: String - let backgroundColors: [UIColor] - let viewTapped: () -> Void - - func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { - return BarScreenViewController.description(for: self, environment: environment) - } -} - -private final class BarScreenViewController: ScreenViewController { - private lazy var titleLabel: UILabel = .init() - private lazy var tapGestureRecognizer: UITapGestureRecognizer = .init() - private var gradientLayer: CAGradientLayer? - - override func viewDidLoad() { - super.viewDidLoad() - - tapGestureRecognizer.addTarget(self, action: #selector(viewTapped)) - view.addGestureRecognizer(tapGestureRecognizer) - - titleLabel.translatesAutoresizingMaskIntoConstraints = false - titleLabel.textAlignment = .center - titleLabel.textColor = .white - view.addSubview(titleLabel) - - NSLayoutConstraint.activate([ - titleLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor), - titleLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor), - ]) - - updateGradient(for: view, colors: screen.backgroundColors) - } - - override func screenDidChange(from previousScreen: BarScreen, previousEnvironment: ViewEnvironment) { - super.screenDidChange(from: previousScreen, previousEnvironment: previousEnvironment) - - titleLabel.text = screen.title - - updateGradient(for: view, colors: screen.backgroundColors) - } - - private func updateGradient(for targetView: UIView, colors: [UIColor]) { - let newGradientLayer = CAGradientLayer() - - newGradientLayer.frame = targetView.bounds - newGradientLayer.colors = colors.map { $0.cgColor } - - targetView.layer.insertSublayer(newGradientLayer, at: 0) - - gradientLayer?.removeFromSuperlayer() - - gradientLayer = newGradientLayer - } - - @objc - private func viewTapped() { - screen.viewTapped() - } -} diff --git a/swift/Samples/SplitScreenContainer/DemoApp/DemoWorkflow.swift b/swift/Samples/SplitScreenContainer/DemoApp/DemoWorkflow.swift deleted file mode 100644 index 0a3d6057a..000000000 --- a/swift/Samples/SplitScreenContainer/DemoApp/DemoWorkflow.swift +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 SplitScreenContainer -import Workflow -import WorkflowUI - -// MARK: Input and Output - -struct DemoWorkflow: Workflow { - typealias Output = Never -} - -// MARK: State and Initialization - -extension DemoWorkflow { - typealias State = Int - - func makeInitialState() -> State { - return 1 - } -} - -// MARK: Actions - -extension DemoWorkflow { - enum Action: WorkflowAction { - typealias WorkflowType = DemoWorkflow - - case viewTapped - - func apply(toState state: inout DemoWorkflow.State) -> Never? { - switch self { - case .viewTapped: - state += 1 - } - - return nil - } - } -} - -// MARK: Rendering - -extension DemoWorkflow { - typealias Rendering = SplitScreenContainerScreen - - private static let sizes: [CGFloat] = [.quarter, .third, .half, 0.75] - private static let colors: [UIColor] = [.red, .blue, .green, .yellow] - private static let complimentaryColors: [UIColor] = [.blue, .green, .yellow, .purple] - - func render(state: State, context: RenderContext) -> Rendering { - let sink = context.makeSink(of: Action.self) - - return SplitScreenContainerScreen( - leadingScreen: leadingScreenFor(state: state, context: context), - trailingScreen: FooScreen(title: "Trailing screen", backgroundColor: .green, viewTapped: { sink.send(.viewTapped) }), - ratio: DemoWorkflow.sizes[state % DemoWorkflow.sizes.count], - separatorColor: .black, - separatorWidth: 1.0 * CGFloat(state) - ) - } - - private func leadingScreenFor(state: State, context: RenderContext) -> AnyScreen { - let sink = context.makeSink(of: Action.self) - - let color = DemoWorkflow.colors[state % DemoWorkflow.colors.count] - - if state % 2 == 0 { - return AnyScreen( - FooScreen( - title: "Leading Foo screen", - backgroundColor: color, - viewTapped: { sink.send(.viewTapped) } - ) - ) - } else { - let complimentaryColor = DemoWorkflow.complimentaryColors[state % DemoWorkflow.complimentaryColors.count] - - return AnyScreen( - BarScreen( - title: "Leading Bar screen", - backgroundColors: [color, complimentaryColor], - viewTapped: { sink.send(.viewTapped) } - ) - ) - } - } -} diff --git a/swift/Samples/SplitScreenContainer/DemoApp/FooScreen.swift b/swift/Samples/SplitScreenContainer/DemoApp/FooScreen.swift deleted file mode 100644 index dd4800987..000000000 --- a/swift/Samples/SplitScreenContainer/DemoApp/FooScreen.swift +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 Workflow -import WorkflowUI - -struct FooScreen: Screen { - let title: String - let backgroundColor: UIColor - let viewTapped: () -> Void - - func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { - return FooScreenViewController.description(for: self, environment: environment) - } -} - -private final class FooScreenViewController: ScreenViewController { - private lazy var titleLabel: UILabel = .init() - private lazy var tapGestureRecognizer: UITapGestureRecognizer = .init() - - override func viewDidLoad() { - super.viewDidLoad() - - tapGestureRecognizer.addTarget(self, action: #selector(viewTapped)) - view.addGestureRecognizer(tapGestureRecognizer) - - titleLabel.translatesAutoresizingMaskIntoConstraints = false - titleLabel.textAlignment = .center - view.addSubview(titleLabel) - - NSLayoutConstraint.activate([ - titleLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor), - titleLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor), - ]) - } - - override func screenDidChange(from previousScreen: FooScreen, previousEnvironment: ViewEnvironment) { - super.screenDidChange(from: previousScreen, previousEnvironment: previousEnvironment) - - view.backgroundColor = screen.backgroundColor - titleLabel.text = screen.title - } - - @objc - private func viewTapped() { - screen.viewTapped() - } -} diff --git a/swift/Samples/SplitScreenContainer/README.md b/swift/Samples/SplitScreenContainer/README.md deleted file mode 100644 index ed3d3078a..000000000 --- a/swift/Samples/SplitScreenContainer/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# SplitScreenContainer - -Container to display two screens side by side. diff --git a/swift/Samples/SplitScreenContainer/SnapshotTests/SplitScreenContainerScreenSnapshotTests.swift b/swift/Samples/SplitScreenContainer/SnapshotTests/SplitScreenContainerScreenSnapshotTests.swift deleted file mode 100644 index 2e835df76..000000000 --- a/swift/Samples/SplitScreenContainer/SnapshotTests/SplitScreenContainerScreenSnapshotTests.swift +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 FBSnapshotTestCase -import Workflow -import WorkflowUI -import XCTest -@testable import SplitScreenContainer - -class SplitScreenContainerScreenSnapshotTests: FBSnapshotTestCase { - override func setUp() { - super.setUp() - recordMode = false - folderName = "SplitScreenContainerScreenSnapshotTests" - fileNameOptions = [.device] - } - - func test_splitRatio() { - let ratios: [String: CGFloat] = [ - "third": .third, - "quarter": .quarter, - "half": .half, - "custom": 0.3125, - ] - - for (name, ratio) in ratios { - let splitScreenContainerScreen = SplitScreenContainerScreen( - leadingScreen: FooScreen(title: "Leading screen", backgroundColor: .green, viewTapped: {}), - trailingScreen: FooScreen(title: "Trailing screen", backgroundColor: .red, viewTapped: {}), - ratio: ratio - ) - - let viewController = SplitScreenContainerViewController( - screen: splitScreenContainerScreen, - environment: .empty - ) - viewController.view.layoutIfNeeded() - - FBSnapshotVerifyView(viewController.view, identifier: name, suffixes: ["_64"]) - } - } -} - -private struct FooScreen: Screen { - let title: String - let backgroundColor: UIColor - let viewTapped: () -> Void - - func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { - return FooScreenViewController.description(for: self, environment: environment) - } -} - -private final class FooScreenViewController: ScreenViewController { - private lazy var titleLabel: UILabel = .init() - private lazy var tapGestureRecognizer: UITapGestureRecognizer = .init() - - override func viewDidLoad() { - super.viewDidLoad() - - tapGestureRecognizer.addTarget(self, action: #selector(viewTapped)) - view.addGestureRecognizer(tapGestureRecognizer) - - titleLabel.translatesAutoresizingMaskIntoConstraints = false - titleLabel.textAlignment = .center - view.addSubview(titleLabel) - - NSLayoutConstraint.activate([ - titleLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor), - titleLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor), - ]) - } - - override func screenDidChange(from previousScreen: FooScreen, previousEnvironment: ViewEnvironment) { - super.screenDidChange(from: previousScreen, previousEnvironment: previousEnvironment) - - view.backgroundColor = screen.backgroundColor - titleLabel.text = screen.title - } - - @objc - private func viewTapped() { - screen.viewTapped() - } -} diff --git a/swift/Samples/SplitScreenContainer/Sources/ContainerView.swift b/swift/Samples/SplitScreenContainer/Sources/ContainerView.swift deleted file mode 100644 index 85a0f1338..000000000 --- a/swift/Samples/SplitScreenContainer/Sources/ContainerView.swift +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 UIKit - -internal class ContainerView: UIView { - var contentView: UIView = .init() - - override init(frame: CGRect) { - super.init(frame: frame) - - commonInit() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - - commonInit() - } - - private func commonInit() { - addSubview(contentView) - } - - override func layoutSubviews() { - super.layoutSubviews() - - contentView.frame = bounds - - contentView.subviews.forEach { $0.frame = self.bounds } - } -} diff --git a/swift/Samples/SplitScreenContainer/Sources/Environment+SplitScreen.swift b/swift/Samples/SplitScreenContainer/Sources/Environment+SplitScreen.swift deleted file mode 100644 index 26d79420f..000000000 --- a/swift/Samples/SplitScreenContainer/Sources/Environment+SplitScreen.swift +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 WorkflowUI - -public enum SplitScreenPosition { - /// Not appearing in a split screen context - case none - - /// Appearing in the leading position in a split screen - case leading - - /// Appearing in the trailing position in a split screen - case trailing -} - -extension ViewEnvironment { - public internal(set) var splitScreenPosition: SplitScreenPosition { - get { return self[SplitScreenPositionKey.self] } - set { self[SplitScreenPositionKey.self] = newValue } - } -} - -private enum SplitScreenPositionKey: ViewEnvironmentKey { - static var defaultValue: SplitScreenPosition = .none -} diff --git a/swift/Samples/SplitScreenContainer/Sources/SplitScreenContainerScreen.swift b/swift/Samples/SplitScreenContainer/Sources/SplitScreenContainerScreen.swift deleted file mode 100644 index 546838b84..000000000 --- a/swift/Samples/SplitScreenContainer/Sources/SplitScreenContainerScreen.swift +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 WorkflowUI - -/// A `SplitScreenContainerScreen` displays two screens side by side with a separator in between. -public struct SplitScreenContainerScreen: Screen { - /// The screen displayed leading the separator. - public let leadingScreen: LeadingScreenType - - /// The screen displayed trailing the separator. - public let trailingScreen: TrailingScreenType - - /// The ratio of `leadingScreen`'s width relative to that of `trailingScreen`. Defaults to `.third`. - public let ratio: CGFloat - - /// The color of the `separatorView` displayed between `leadingScreen`'s and `trailingScreen`'s views. - public let separatorColor: UIColor - - /// The width of the `separatorView` displayed between `leadingScreen`'s and `trailingScreen`'s views. - public let separatorWidth: CGFloat - - public init( - leadingScreen: LeadingScreenType, - trailingScreen: TrailingScreenType, - ratio: CGFloat = .third, - separatorColor: UIColor = .black, - separatorWidth: CGFloat = 1.0 - ) { - self.leadingScreen = leadingScreen - self.trailingScreen = trailingScreen - self.ratio = ratio - self.separatorColor = separatorColor - self.separatorWidth = separatorWidth - } - - public func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { - return SplitScreenContainerViewController.description(for: self, environment: environment) - } -} - -public extension CGFloat { - static let quarter: CGFloat = 1.0 / 4.0 - static let third: CGFloat = 1.0 / 3.0 - static let half: CGFloat = 1.0 / 2.0 -} diff --git a/swift/Samples/SplitScreenContainer/Sources/SplitScreenContainerViewController.swift b/swift/Samples/SplitScreenContainer/Sources/SplitScreenContainerViewController.swift deleted file mode 100644 index 05888b6f1..000000000 --- a/swift/Samples/SplitScreenContainer/Sources/SplitScreenContainerViewController.swift +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 Workflow -import WorkflowUI - -internal final class SplitScreenContainerViewController: ScreenViewController { - internal typealias ContainerScreen = SplitScreenContainerScreen - - private var leadingContentViewController: DescribedViewController - private lazy var leadingContainerView: ContainerView = .init() - - private lazy var separatorView: UIView = .init() - - private var trailingContentViewController: DescribedViewController - private lazy var trailingContainerView: ContainerView = .init() - - private var needsAnimatedLayout = false - - required init(screen: ContainerScreen, environment: ViewEnvironment) { - self.leadingContentViewController = DescribedViewController( - screen: screen.leadingScreen, - environment: environment - .setting(keyPath: \.splitScreenPosition, to: .leading) - ) - self.trailingContentViewController = DescribedViewController( - screen: screen.trailingScreen, - environment: environment - .setting(keyPath: \.splitScreenPosition, to: .trailing) - ) - super.init(screen: screen, environment: environment) - } - - override internal func screenDidChange(from previousScreen: ContainerScreen, previousEnvironment: ViewEnvironment) { - super.screenDidChange(from: previousScreen, previousEnvironment: previousEnvironment) - - if screen.ratio != previousScreen.ratio { - needsAnimatedLayout = true - } - if screen.separatorWidth != previousScreen.separatorWidth { - needsAnimatedLayout = true - } - update(with: screen) - } - - private func update(with screen: ContainerScreen) { - separatorView.backgroundColor = screen.separatorColor - - leadingContentViewController.update( - screen: screen.leadingScreen, - environment: environment - .setting(keyPath: \.splitScreenPosition, to: .leading) - ) - trailingContentViewController.update( - screen: screen.trailingScreen, - environment: environment - .setting(keyPath: \.splitScreenPosition, to: .trailing) - ) - - // Intentional force of layout pass after updating the child view controllers - view.layoutIfNeeded() - - if needsAnimatedLayout { - needsAnimatedLayout = false - - UIView.animate(withDuration: 0.25) { - self.view.setNeedsLayout() - self.view.layoutIfNeeded() - } - } - } - - override internal func viewDidLoad() { - super.viewDidLoad() - - view.addSubview(leadingContainerView) - view.addSubview(separatorView) - view.addSubview(trailingContainerView) - - addChild(leadingContentViewController) - leadingContainerView.contentView.addSubview(leadingContentViewController.view) - leadingContentViewController.didMove(toParent: self) - - addChild(trailingContentViewController) - trailingContainerView.contentView.addSubview(trailingContentViewController.view) - trailingContentViewController.didMove(toParent: self) - - update(with: screen) - } - - override internal func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - - let distance = view.bounds.width * screen.ratio - - let (firstSlice, trailingRect) = view.bounds.divided(atDistance: distance, from: .minXEdge) - - let (leadingRect, separatorRect) = firstSlice.divided(atDistance: distance - screen.separatorWidth, from: .minXEdge) - - leadingContainerView.frame = isLayoutDirectionRightToLeft ? trailingRect : leadingRect - - separatorView.frame = separatorRect - - trailingContainerView.frame = isLayoutDirectionRightToLeft ? leadingRect : trailingRect - } -} - -private extension UIViewController { - var isLayoutDirectionRightToLeft: Bool { - if #available(iOS 10.0, *) { - return traitCollection.layoutDirection == .rightToLeft - } else { - return UIView.userInterfaceLayoutDirection(for: view.semanticContentAttribute) == .rightToLeft - } - } -} diff --git a/swift/Samples/SplitScreenContainer/SplitScreenContainer.podspec b/swift/Samples/SplitScreenContainer/SplitScreenContainer.podspec deleted file mode 100644 index 0ac5a5a25..000000000 --- a/swift/Samples/SplitScreenContainer/SplitScreenContainer.podspec +++ /dev/null @@ -1,41 +0,0 @@ -Pod::Spec.new do |s| - s.name = 'SplitScreenContainer' - s.version = '1.0.0.LOCAL' - s.summary = 'See the README.' - s.homepage = 'https://www.github.com/square/workflow' - s.license = 'Apache License, Version 2.0' - s.author = 'Square' - s.source = { git: 'Not Published', tag: "podify/#{s.version}" } - - # 1.7 is needed for `swift_versions` support - s.cocoapods_version = '>= 1.7.0' - - s.swift_versions = ['5.0'] - s.ios.deployment_target = '10.0' - - s.source_files = 'Sources/**/*.swift' - - s.dependency 'Workflow' - s.dependency 'WorkflowUI' - - s.app_spec 'DemoApp' do |app_spec| - app_spec.source_files = 'DemoApp/**/*.swift' - end - - s.test_spec 'SnapshotTests' do |test_spec| - test_spec.requires_app_host = true - test_spec.source_files = 'SnapshotTests/**/*.swift' - - test_spec.framework = 'XCTest' - - test_spec.dependency 'iOSSnapshotTestCase' - - test_spec.scheme = { - environment_variables: { - 'FB_REFERENCE_IMAGE_DIR' => '$PODS_TARGET_SRCROOT/SnapshotTests/ReferenceImages', - 'IMAGE_DIFF_DIR' => '$PODS_TARGET_SRCROOT/SnapshotTests/FailureDiffs' - } - } - end - -end diff --git a/swift/Samples/TicTacToe/.gitignore b/swift/Samples/TicTacToe/.gitignore deleted file mode 100644 index 05ef11923..000000000 --- a/swift/Samples/TicTacToe/.gitignore +++ /dev/null @@ -1 +0,0 @@ -Podfile.lock diff --git a/swift/Samples/TicTacToe/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/swift/Samples/TicTacToe/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index d8db8d65f..000000000 --- a/swift/Samples/TicTacToe/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,98 +0,0 @@ -{ - "images" : [ - { - "idiom" : "iphone", - "size" : "20x20", - "scale" : "2x" - }, - { - "idiom" : "iphone", - "size" : "20x20", - "scale" : "3x" - }, - { - "idiom" : "iphone", - "size" : "29x29", - "scale" : "2x" - }, - { - "idiom" : "iphone", - "size" : "29x29", - "scale" : "3x" - }, - { - "idiom" : "iphone", - "size" : "40x40", - "scale" : "2x" - }, - { - "idiom" : "iphone", - "size" : "40x40", - "scale" : "3x" - }, - { - "idiom" : "iphone", - "size" : "60x60", - "scale" : "2x" - }, - { - "idiom" : "iphone", - "size" : "60x60", - "scale" : "3x" - }, - { - "idiom" : "ipad", - "size" : "20x20", - "scale" : "1x" - }, - { - "idiom" : "ipad", - "size" : "20x20", - "scale" : "2x" - }, - { - "idiom" : "ipad", - "size" : "29x29", - "scale" : "1x" - }, - { - "idiom" : "ipad", - "size" : "29x29", - "scale" : "2x" - }, - { - "idiom" : "ipad", - "size" : "40x40", - "scale" : "1x" - }, - { - "idiom" : "ipad", - "size" : "40x40", - "scale" : "2x" - }, - { - "idiom" : "ipad", - "size" : "76x76", - "scale" : "1x" - }, - { - "idiom" : "ipad", - "size" : "76x76", - "scale" : "2x" - }, - { - "idiom" : "ipad", - "size" : "83.5x83.5", - "scale" : "2x" - }, - { - "idiom" : "ios-marketing", - "size" : "1024x1024", - "scale" : "1x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/swift/Samples/TicTacToe/Resources/Assets.xcassets/Contents.json b/swift/Samples/TicTacToe/Resources/Assets.xcassets/Contents.json deleted file mode 100644 index da4a164c9..000000000 --- a/swift/Samples/TicTacToe/Resources/Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/swift/Samples/TicTacToe/Resources/Base.lproj/LaunchScreen.storyboard b/swift/Samples/TicTacToe/Resources/Base.lproj/LaunchScreen.storyboard deleted file mode 100644 index bfa361294..000000000 --- a/swift/Samples/TicTacToe/Resources/Base.lproj/LaunchScreen.storyboard +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/swift/Samples/TicTacToe/Sources/AppDelegate.swift b/swift/Samples/TicTacToe/Sources/AppDelegate.swift deleted file mode 100644 index 54b5e87cd..000000000 --- a/swift/Samples/TicTacToe/Sources/AppDelegate.swift +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 BackStackContainer -import UIKit -import WorkflowUI - -@UIApplicationMain -class AppDelegate: UIResponder, UIApplicationDelegate { - var window: UIWindow? - - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - window = UIWindow(frame: UIScreen.main.bounds) - - window?.rootViewController = ContainerViewController(workflow: MainWorkflow()) - - window?.makeKeyAndVisible() - - return true - } - - func applicationWillResignActive(_ application: UIApplication) {} - - func applicationDidEnterBackground(_ application: UIApplication) {} - - func applicationWillEnterForeground(_ application: UIApplication) {} - - func applicationDidBecomeActive(_ application: UIApplication) {} - - func applicationWillTerminate(_ application: UIApplication) {} -} diff --git a/swift/Samples/TicTacToe/Sources/Authentication/AuthenticationService.swift b/swift/Samples/TicTacToe/Sources/Authentication/AuthenticationService.swift deleted file mode 100644 index b86d6d0ae..000000000 --- a/swift/Samples/TicTacToe/Sources/Authentication/AuthenticationService.swift +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 ReactiveSwift - -final class AuthenticationService { - static let delayMS: TimeInterval = 0.750 - static let weakToken = "Need a second factor there, friend" - static let realToken = "Welcome aboard!" - static let secondFactor = "1234" - - func login(email: String, password: String) -> SignalProducer { - if password == "password" { - if email.contains("2fa") { - return SignalProducer(value: AuthenticationResponse( - token: AuthenticationService.weakToken, - secondFactorRequired: true - )) - .delay(AuthenticationService.delayMS, on: QueueScheduler.main) - } else { - return SignalProducer(value: AuthenticationResponse( - token: AuthenticationService.realToken, secondFactorRequired: false - )) - .delay(AuthenticationService.delayMS, on: QueueScheduler.main) - } - } else { - return SignalProducer(error: .invalidUserPassword) - .delay(AuthenticationService.delayMS, on: QueueScheduler.main) - } - } - - func secondFactor(token: String, secondFactor: String) -> SignalProducer { - if token != AuthenticationService.weakToken { - return SignalProducer(error: .invalidIntermediateToken) - .delay(AuthenticationService.delayMS, on: QueueScheduler.main) - } else if secondFactor != AuthenticationService.secondFactor { - return SignalProducer(error: .invalidTwoFactor) - .delay(AuthenticationService.delayMS, on: QueueScheduler.main) - } else { - return SignalProducer(value: AuthenticationResponse( - token: AuthenticationService.realToken, - secondFactorRequired: false - )) - .delay(AuthenticationService.delayMS, on: QueueScheduler.main) - } - } -} - -extension AuthenticationService { - enum AuthenticationError: Error { - var localizedDescription: String { - switch self { - case .invalidUserPassword: - return "Unknown user or invalid password." - case .invalidTwoFactor: - return "Invalid second factor (try \(AuthenticationService.secondFactor))" - case .invalidIntermediateToken: - return "404!! What happened to your token there bud?!?!" - } - } - - case invalidUserPassword - case invalidTwoFactor - case invalidIntermediateToken - } - - struct AuthenticationResponse { - var token: String - var secondFactorRequired: Bool - } -} diff --git a/swift/Samples/TicTacToe/Sources/Authentication/AuthenticationWorkflow.swift b/swift/Samples/TicTacToe/Sources/Authentication/AuthenticationWorkflow.swift deleted file mode 100644 index 4e15c9f4d..000000000 --- a/swift/Samples/TicTacToe/Sources/Authentication/AuthenticationWorkflow.swift +++ /dev/null @@ -1,265 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 AlertContainer -import BackStackContainer -import ModalContainer -import ReactiveSwift -import Workflow -import WorkflowUI - -// MARK: Input and Output - -struct AuthenticationWorkflow: Workflow { - var authenticationService: AuthenticationService - - enum Output { - case authorized(session: String) - } -} - -// MARK: State and Initialization - -extension AuthenticationWorkflow { - enum State: Equatable { - case emailPassword - case authenticationErrorAlert(error: AuthenticationService.AuthenticationError?) - case authorizingEmailPassword(email: String, password: String) - case twoFactor(intermediateSession: String, authenticationError: AuthenticationService.AuthenticationError?) - case authorizingTwoFactor(twoFactorCode: String, intermediateSession: String) - } - - func makeInitialState() -> AuthenticationWorkflow.State { - return .emailPassword - } -} - -// MARK: Actions - -extension AuthenticationWorkflow { - enum Action: WorkflowAction { - typealias WorkflowType = AuthenticationWorkflow - - case back - case login(email: String, password: String) - case verifySecondFactor(intermediateSession: String, twoFactorCode: String) - case authenticationSucceeded(response: AuthenticationService.AuthenticationResponse) - case authenticationError(AuthenticationService.AuthenticationError) - case dismissAuthenticationAlert - - func apply(toState state: inout AuthenticationWorkflow.State) -> AuthenticationWorkflow.Output? { - switch self { - case .back: - switch state { - case .twoFactor: - state = .emailPassword - - default: - fatalError("Unexpected back in state \(state)") - } - - case let .login(email: email, password: password): - state = .authorizingEmailPassword(email: email, password: password) - - case let .verifySecondFactor(intermediateSession: intermediateSession, twoFactorCode: twoFactorCode): - state = .authorizingTwoFactor(twoFactorCode: twoFactorCode, intermediateSession: intermediateSession) - - case let .authenticationSucceeded(response: response): - if response.secondFactorRequired { - state = .twoFactor(intermediateSession: response.token, authenticationError: nil) - } else { - return .authorized(session: response.token) - } - - case .dismissAuthenticationAlert: - state = .emailPassword - - case let .authenticationError(error): - switch state { - case .authorizingEmailPassword: - state = .authenticationErrorAlert(error: error) - case .authorizingTwoFactor(twoFactorCode: _, intermediateSession: let intermediateSession): - state = .twoFactor(intermediateSession: intermediateSession, authenticationError: error) - - default: - fatalError("Unexpected authentication error in state \(state)") - } - } - return nil - } - } -} - -// MARK: Workers - -extension AuthenticationWorkflow { - struct AuthorizingEmailPasswordWorker: Worker { - typealias Output = Action - - var authenticationService: AuthenticationService - var email: String - var password: String - - func run() -> SignalProducer { - return authenticationService - .login(email: email, password: password) - .map { response -> Action in - .authenticationSucceeded(response: response) - } - .flatMapError { - SignalProducer(value: .authenticationError($0)) - } - } - - func isEquivalent(to otherWorker: AuthorizingEmailPasswordWorker) -> Bool { - return email == otherWorker.email - && password == otherWorker.password - } - } - - struct AuthorizingTwoFactorWorker: Worker { - typealias Output = Action - - var authenticationService: AuthenticationService - var intermediateToken: String - var twoFactorCode: String - - func run() -> SignalProducer { - return authenticationService - .secondFactor( - token: intermediateToken, - secondFactor: twoFactorCode - ) - .map { - .authenticationSucceeded(response: $0) - } - .flatMapError { - SignalProducer(value: .authenticationError($0)) - } - } - - func isEquivalent(to otherWorker: AuthenticationWorkflow.AuthorizingTwoFactorWorker) -> Bool { - return intermediateToken == otherWorker.intermediateToken - && twoFactorCode == otherWorker.twoFactorCode - } - } -} - -// MARK: Rendering - -extension AuthenticationWorkflow { - typealias Rendering = AlertContainerScreen>> - - func render(state: AuthenticationWorkflow.State, context: RenderContext) -> Rendering { - let sink = context.makeSink(of: Action.self) - - var backStackItems: [BackStackScreen.Item] = [] - var modals: [ModalContainerScreenModal] = [] - var alert: Alert? - - let loginScreen = LoginWorkflow().mapOutput { output -> Action in - switch output { - case let .login(email: email, password: password): - return .login(email: email, password: password) - } - }.rendered(with: context) - backStackItems.append(BackStackScreen.Item(screen: loginScreen.asAnyScreen(), barVisibility: .hidden)) - - switch state { - case .emailPassword: - break - - case let .authenticationErrorAlert(error: error): - if let error = error { - alert = Alert( - title: "Error", - message: error.localizedDescription, - actions: [AlertAction( - title: "Ok", - style: AlertAction.Style.default, - handler: { - sink.send(.dismissAuthenticationAlert) - } - )] - ) - } - - case let .authorizingEmailPassword(email: email, password: password): - context.awaitResult(for: AuthorizingEmailPasswordWorker( - authenticationService: authenticationService, - email: email, - password: password - )) - modals.append(ModalContainerScreenModal(screen: AnyScreen(LoadingScreen()), style: .fullScreen, key: "", animated: false)) - - case let .twoFactor(intermediateSession: intermediateSession, authenticationError: authenticationError): - backStackItems.append(twoFactorScreen( - error: authenticationError, - intermediateSession: intermediateSession, - sink: sink - )) - - case let .authorizingTwoFactor(twoFactorCode: twoFactorCode, intermediateSession: intermediateSession): - context.awaitResult( - for: AuthorizingTwoFactorWorker( - authenticationService: authenticationService, - intermediateToken: intermediateSession, - twoFactorCode: twoFactorCode - )) - - backStackItems.append(twoFactorScreen(error: nil, intermediateSession: intermediateSession, sink: sink)) - modals.append(ModalContainerScreenModal(screen: AnyScreen(LoadingScreen()), style: .fullScreen, key: "", animated: false)) - } - return AlertContainerScreen( - baseScreen: ModalContainerScreen( - baseScreen: BackStackScreen( - items: backStackItems), - modals: modals - ), - alert: alert - ) - } - - private func twoFactorScreen(error: AuthenticationService.AuthenticationError?, intermediateSession: String, sink: Sink) -> BackStackScreen.Item { - let title: String - if let authenticationError = error { - title = authenticationError.localizedDescription - } else { - title = "Enter the one time code to continue" - } - - let twoFactorScreen = TwoFactorScreen( - title: title, - onLoginTapped: { twoFactorCode in - sink.send(.verifySecondFactor( - intermediateSession: intermediateSession, - twoFactorCode: twoFactorCode - )) - } - ) - - return BackStackScreen.Item( - screen: twoFactorScreen.asAnyScreen(), - barVisibility: .visible(BackStackScreen.BarContent( - leftItem: BackStackScreen.BarContent.BarButtonItem.button(BackStackScreen.BarContent.Button( - content: .text("Cancel"), - handler: { - sink.send(.back) - } - )))) - ) - } -} diff --git a/swift/Samples/TicTacToe/Sources/Authentication/LoadingScreen.swift b/swift/Samples/TicTacToe/Sources/Authentication/LoadingScreen.swift deleted file mode 100644 index d38a5cb95..000000000 --- a/swift/Samples/TicTacToe/Sources/Authentication/LoadingScreen.swift +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 WorkflowUI - -struct LoadingScreen: Screen { - func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { - return LoadingScreenViewController.description(for: self, environment: environment) - } -} - -private final class LoadingScreenViewController: ScreenViewController { - let loadingLabel = UILabel(frame: .zero) - - override func viewDidLoad() { - super.viewDidLoad() - - loadingLabel.font = UIFont.boldSystemFont(ofSize: 44.0) - loadingLabel.textColor = .black - loadingLabel.textAlignment = .center - loadingLabel.text = "Loading..." - - view.backgroundColor = .white - - view.addSubview(loadingLabel) - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - - loadingLabel.frame = view.bounds - } -} diff --git a/swift/Samples/TicTacToe/Sources/Authentication/LoginScreen.swift b/swift/Samples/TicTacToe/Sources/Authentication/LoginScreen.swift deleted file mode 100644 index 12ff38a80..000000000 --- a/swift/Samples/TicTacToe/Sources/Authentication/LoginScreen.swift +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 Workflow -import WorkflowUI - -struct LoginScreen: Screen { - var title: String - var email: String - var onEmailChanged: (String) -> Void - var password: String - var onPasswordChanged: (String) -> Void - var onLoginTapped: () -> Void - - func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { - return ViewControllerDescription( - build: { LoginViewController() }, - update: { $0.update(with: self) } - ) - } -} - -private final class LoginViewController: UIViewController { - private let welcomeLabel: UILabel = UILabel(frame: .zero) - private let emailField: UITextField = UITextField(frame: .zero) - private let passwordField: UITextField = UITextField(frame: .zero) - private let button: UIButton = UIButton(frame: .zero) - private var onEmailChanged: (String) -> Void = { _ in } - private var onPasswordChanged: (String) -> Void = { _ in } - private var onLoginTapped: () -> Void = {} - - override func viewDidLoad() { - super.viewDidLoad() - - view.backgroundColor = .white - - welcomeLabel.textAlignment = .center - - emailField.placeholder = "email@address.com" - emailField.autocapitalizationType = .none - emailField.autocorrectionType = .no - emailField.textContentType = .emailAddress - emailField.backgroundColor = UIColor(white: 0.92, alpha: 1.0) - emailField.addTarget(self, action: #selector(textDidChange(sender:)), for: .editingChanged) - - passwordField.placeholder = "password" - passwordField.isSecureTextEntry = true - passwordField.backgroundColor = UIColor(white: 0.92, alpha: 1.0) - passwordField.addTarget(self, action: #selector(textDidChange(sender:)), for: .editingChanged) - - button.backgroundColor = UIColor(red: 41 / 255, green: 150 / 255, blue: 204 / 255, alpha: 1.0) - button.setTitle("Login", for: .normal) - button.addTarget(self, action: #selector(buttonTapped(sender:)), for: .touchUpInside) - - view.addSubview(welcomeLabel) - view.addSubview(emailField) - view.addSubview(passwordField) - view.addSubview(button) - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - - let inset: CGFloat = 12.0 - let height: CGFloat = 44.0 - var yOffset = (view.bounds.size.height - (3 * height + inset)) / 2.0 - - welcomeLabel.frame = CGRect( - x: view.bounds.origin.x, - y: view.bounds.origin.y, - width: view.bounds.size.width, - height: yOffset - ) - - emailField.frame = CGRect( - x: view.bounds.origin.x, - y: yOffset, - width: view.bounds.size.width, - height: height - ) - .insetBy(dx: inset, dy: 0.0) - - yOffset += height + inset - - passwordField.frame = CGRect( - x: view.bounds.origin.x, - y: yOffset, - width: view.bounds.size.width, - height: height - ) - .insetBy(dx: inset, dy: 0.0) - - yOffset += height + inset - - button.frame = CGRect( - x: view.bounds.origin.x, - y: yOffset, - width: view.bounds.size.width, - height: height - ) - .insetBy(dx: inset, dy: 0.0) - } - - func update(with screen: LoginScreen) { - welcomeLabel.text = screen.title - emailField.text = screen.email - passwordField.text = screen.password - onEmailChanged = screen.onEmailChanged - onPasswordChanged = screen.onPasswordChanged - onLoginTapped = screen.onLoginTapped - } - - @objc private func textDidChange(sender: UITextField) { - guard let text = sender.text else { - return - } - if sender == emailField { - onEmailChanged(text) - } else if sender == passwordField { - onPasswordChanged(text) - } - } - - @objc private func buttonTapped(sender: UIButton) { - onLoginTapped() - } -} diff --git a/swift/Samples/TicTacToe/Sources/Authentication/LoginWorkflow.swift b/swift/Samples/TicTacToe/Sources/Authentication/LoginWorkflow.swift deleted file mode 100644 index e39014f5f..000000000 --- a/swift/Samples/TicTacToe/Sources/Authentication/LoginWorkflow.swift +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 ReactiveSwift -import Workflow -import WorkflowUI - -// MARK: Input and Output - -struct LoginWorkflow: Workflow { - enum Output { - case login(email: String, password: String) - } -} - -// MARK: State and Initialization - -extension LoginWorkflow { - struct State { - var email: String - var password: String - } - - func makeInitialState() -> LoginWorkflow.State { - return State(email: "", password: "") - } -} - -// MARK: Actions - -extension LoginWorkflow { - enum Action: WorkflowAction { - typealias WorkflowType = LoginWorkflow - - case emailUpdated(String) - case passwordUpdated(String) - case login - - func apply(toState state: inout LoginWorkflow.State) -> LoginWorkflow.Output? { - switch self { - case let .emailUpdated(email): - state.email = email - - case let .passwordUpdated(password): - state.password = password - - case .login: - return .login(email: state.email, password: state.password) - } - - return nil - } - } -} - -// MARK: Rendering - -extension LoginWorkflow { - typealias Rendering = LoginScreen - - func render(state: LoginWorkflow.State, context: RenderContext) -> Rendering { - let sink = context.makeSink(of: Action.self) - - return LoginScreen( - title: "Welcome! Please log in to play TicTacToe!", - email: state.email, - onEmailChanged: { email in - sink.send(.emailUpdated(email)) - }, - password: state.password, - onPasswordChanged: { password in - sink.send(.passwordUpdated(password)) - }, - onLoginTapped: { - sink.send(.login) - } - ) - } -} diff --git a/swift/Samples/TicTacToe/Sources/Authentication/TwoFactorScreen.swift b/swift/Samples/TicTacToe/Sources/Authentication/TwoFactorScreen.swift deleted file mode 100644 index 41ca18e72..000000000 --- a/swift/Samples/TicTacToe/Sources/Authentication/TwoFactorScreen.swift +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 WorkflowUI - -struct TwoFactorScreen: Screen { - var title: String - var onLoginTapped: (String) -> Void - - func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { - return TwoFactorViewController.description(for: self, environment: environment) - } -} - -private final class TwoFactorViewController: ScreenViewController { - let titleLabel = UILabel(frame: .zero) - let twoFactorField = UITextField(frame: .zero) - let button = UIButton(frame: .zero) - - override func viewDidLoad() { - super.viewDidLoad() - - titleLabel.textAlignment = .center - - twoFactorField.placeholder = "one time token" - twoFactorField.backgroundColor = UIColor(white: 0.92, alpha: 1.0) - - button.backgroundColor = UIColor(red: 41 / 255, green: 150 / 255, blue: 204 / 255, alpha: 1.0) - button.setTitle("Login", for: .normal) - button.addTarget(self, action: #selector(buttonTapped(sender:)), for: .touchUpInside) - - view.addSubview(titleLabel) - view.addSubview(twoFactorField) - view.addSubview(button) - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - - let inset: CGFloat = 12.0 - let height: CGFloat = 44.0 - - var yOffset = (view.bounds.size.height - (2 * height + inset)) / 2.0 - - titleLabel.frame = CGRect( - x: view.bounds.origin.x, - y: view.bounds.origin.y, - width: view.bounds.size.width, - height: yOffset - ) - - twoFactorField.frame = CGRect( - x: view.bounds.origin.x, - y: yOffset, - width: view.bounds.size.width, - height: height - ) - .insetBy(dx: inset, dy: 0.0) - - yOffset += height + inset - - button.frame = CGRect( - x: view.bounds.origin.x, - y: yOffset, - width: view.bounds.size.width, - height: height - ) - .insetBy(dx: inset, dy: 0.0) - } - - override func screenDidChange(from previousScreen: TwoFactorScreen, previousEnvironment: ViewEnvironment) { - super.screenDidChange(from: previousScreen, previousEnvironment: previousEnvironment) - titleLabel.text = screen.title - } - - @objc private func buttonTapped(sender: UIButton) { - guard let twoFactorCode = twoFactorField.text else { - return - } - screen.onLoginTapped(twoFactorCode) - } -} diff --git a/swift/Samples/TicTacToe/Sources/Game/Board.swift b/swift/Samples/TicTacToe/Sources/Game/Board.swift deleted file mode 100644 index f4b9bdd12..000000000 --- a/swift/Samples/TicTacToe/Sources/Game/Board.swift +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 - -enum Player: Equatable { - case x - case o -} - -struct Board: Equatable { - private(set) var rows: [[Cell]] - - enum Cell: Equatable { - case empty - case taken(Player) - } - - init() { - self.rows = [ - [.empty, .empty, .empty], - [.empty, .empty, .empty], - [.empty, .empty, .empty], - ] - } - - func isFull() -> Bool { - for row in rows { - for col in row { - if col == .empty { - return false - } - } - } - return true - } - - func hasVictory() -> Bool { - var done = false - - // Across - var row = 0 - while !done, row < 3 { - done = - rows[row][0] != .empty - && rows[row][0] == rows[row][1] - && rows[row][0] == rows[row][2] - - row += 1 - } - - // Down - var col = 0 - while !done, col < 3 { - done = - rows[0][col] != .empty - && rows[0][col] == rows[1][col] - && rows[1][col] == rows[2][col] - - col += 1 - } - - // Diagonal - if !done { - done = - rows[0][0] != .empty - && rows[0][0] == rows[1][1] - && rows[0][0] == rows[2][2] - } - - if !done { - done = - rows[0][2] != .empty - && rows[0][2] == rows[1][1] - && rows[0][2] == rows[2][0] - } - - return done - } - - func isEmpty(row: Int, col: Int) -> Bool { - guard row < 3 else { - fatalError("Received an invalid row \(row)") - } - guard col < 3 else { - fatalError("Received an invalid col \(col)") - } - if rows[row][col] == .empty { - return true - } else { - return false - } - } - - mutating func takeSquare(row: Int, col: Int, player: Player) { - guard row < 3 else { - fatalError("Received an invalid row \(row)") - } - guard col < 3 else { - fatalError("Received an invalid col \(col)") - } - guard isEmpty(row: row, col: col) else { - return - } - - rows[row][col] = .taken(player) - } -} diff --git a/swift/Samples/TicTacToe/Sources/Game/ConfirmQuitScreen.swift b/swift/Samples/TicTacToe/Sources/Game/ConfirmQuitScreen.swift deleted file mode 100644 index 826d3d237..000000000 --- a/swift/Samples/TicTacToe/Sources/Game/ConfirmQuitScreen.swift +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 Workflow -import WorkflowUI - -struct ConfirmQuitScreen: Screen { - let question: String - var onQuitTapped: () -> Void = {} - var onCancelTapped: () -> Void = {} - - func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { - return ConfirmQuitViewController.description(for: self, environment: environment) - } -} - -final class ConfirmQuitViewController: ScreenViewController { - private let questionLabel: UILabel = UILabel(frame: .zero) - private let confirmButton: UIButton = UIButton(frame: .zero) - private let cancelButton: UIButton = UIButton(frame: .zero) - private var onQuitTapped: () -> Void = {} - private var onCancelTapped: () -> Void = {} - - override func screenDidChange(from previousScreen: ConfirmQuitScreen, previousEnvironment: ViewEnvironment) { - super.screenDidChange(from: previousScreen, previousEnvironment: previousEnvironment) - - /// Update UI - questionLabel.text = screen.question - onQuitTapped = screen.onQuitTapped - onCancelTapped = screen.onCancelTapped - } - - override func viewDidLoad() { - super.viewDidLoad() - - view.backgroundColor = .white - - questionLabel.textAlignment = .center - - confirmButton.backgroundColor = UIColor(red: 41 / 255, green: 150 / 255, blue: 204 / 255, alpha: 1.0) - confirmButton.setTitle("Yes, quit the game", for: .normal) - confirmButton.addTarget(self, action: #selector(quitButtonTapped(sender:)), for: .touchUpInside) - - cancelButton.backgroundColor = UIColor(red: 41 / 255, green: 150 / 255, blue: 204 / 255, alpha: 1.0) - cancelButton.setTitle("Go back", for: .normal) - cancelButton.addTarget(self, action: #selector(cancelButtonTapped(sender:)), for: .touchUpInside) - - view.addSubview(questionLabel) - view.addSubview(confirmButton) - view.addSubview(cancelButton) - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - - let inset: CGFloat = 12.0 - let height: CGFloat = 44.0 - let buttonHeight: CGFloat = 50.0 - var yOffset = view.bounds.origin.y + view.bounds.size.height / 4 - - questionLabel.frame = CGRect( - x: view.bounds.origin.x, - y: yOffset, - width: view.bounds.size.width, - height: height - ) - - yOffset += height + inset * 2 - - confirmButton.frame = CGRect( - x: view.bounds.origin.x, - y: yOffset, - width: view.bounds.size.width, - height: buttonHeight - ) - .insetBy(dx: inset, dy: 0.0) - - yOffset += height + inset * 2 - - cancelButton.frame = CGRect( - x: view.bounds.origin.x, - y: yOffset, - width: view.bounds.size.width, - height: buttonHeight - ) - .insetBy(dx: inset, dy: 0.0) - } - - @objc private func cancelButtonTapped(sender: UIButton) { - onCancelTapped() - } - - @objc private func quitButtonTapped(sender: UIButton) { - onQuitTapped() - } -} diff --git a/swift/Samples/TicTacToe/Sources/Game/ConfirmQuitWorkflow.swift b/swift/Samples/TicTacToe/Sources/Game/ConfirmQuitWorkflow.swift deleted file mode 100644 index be6979763..000000000 --- a/swift/Samples/TicTacToe/Sources/Game/ConfirmQuitWorkflow.swift +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 AlertContainer -import BackStackContainer -import ReactiveSwift -import Workflow -import WorkflowUI - -// MARK: Input and Output - -struct ConfirmQuitWorkflow: Workflow { - enum Output { - case cancel - case quit - } -} - -// MARK: State and Initialization - -extension ConfirmQuitWorkflow { - struct State { - var step: Step - - enum Step { - case confirmOnce - case confirmTwice - } - } - - func makeInitialState() -> ConfirmQuitWorkflow.State { - return State(step: .confirmOnce) - } -} - -// MARK: Actions - -extension ConfirmQuitWorkflow { - enum Action: WorkflowAction { - case cancel - case quit - case confirm - - typealias WorkflowType = ConfirmQuitWorkflow - - func apply(toState state: inout ConfirmQuitWorkflow.State) -> ConfirmQuitWorkflow.Output? { - switch self { - case .cancel: - return .cancel - - case .quit: - return .quit - case .confirm: - state.step = .confirmTwice - return nil - } - } - } -} - -// MARK: Rendering - -extension ConfirmQuitWorkflow { - typealias Rendering = (ConfirmQuitScreen, Alert?) - - func render(state: ConfirmQuitWorkflow.State, context: RenderContext) -> Rendering { - let sink = context.makeSink(of: Action.self) - var alert: Alert? - - switch state.step { - case .confirmOnce: - break - case .confirmTwice: - alert = Alert( - title: "Confirm Again", - message: "Do you really want to quit?", - actions: [ - AlertAction( - title: "Not really", - style: AlertAction.Style.cancel, - handler: { - sink.send(.cancel) - } - ), - AlertAction( - title: "Yes, please!", - style: AlertAction.Style.destructive, - handler: { - sink.send(.quit) - } - ), - ] - ) - } - - return (ConfirmQuitScreen( - question: "Are you sure you want to quit?", - onQuitTapped: { - sink.send(.confirm) - }, - onCancelTapped: { - sink.send(.cancel) - } - ), alert) - } -} diff --git a/swift/Samples/TicTacToe/Sources/Game/GamePlayScreen.swift b/swift/Samples/TicTacToe/Sources/Game/GamePlayScreen.swift deleted file mode 100644 index 9bfe2ee58..000000000 --- a/swift/Samples/TicTacToe/Sources/Game/GamePlayScreen.swift +++ /dev/null @@ -1,152 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 WorkflowUI - -struct GamePlayScreen: Screen { - var gameState: GameState - var playerX: String - var playerO: String - var board: [[Board.Cell]] - var onSelected: (Int, Int) -> Void - - func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { - return GamePlayViewController.description(for: self, environment: environment) - } -} - -final class GamePlayViewController: ScreenViewController { - let titleLabel: UILabel = UILabel(frame: .zero) - let cells: [[UIButton]] = { - (0 ..< 3).map { _ in - (0 ..< 3).map { _ in UIButton(frame: .zero) } - } - }() - - override func viewDidLoad() { - super.viewDidLoad() - - titleLabel.textAlignment = .center - titleLabel.font = UIFont.systemFont(ofSize: 32.0) - view.addSubview(titleLabel) - - var toggle = true - for row in cells { - for cell in row { - let backgroundColor: UIColor - if toggle { - backgroundColor = UIColor(white: 0.92, alpha: 1.0) - } else { - backgroundColor = UIColor(white: 0.82, alpha: 1.0) - } - cell.backgroundColor = backgroundColor - toggle = !toggle - - cell.titleLabel?.font = UIFont.boldSystemFont(ofSize: 66.0) - cell.addTarget(self, action: #selector(buttonPressed(sender:)), for: .touchUpInside) - view.addSubview(cell) - } - } - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - - let inset: CGFloat = 8.0 - let boardLength = min(view.bounds.width, view.bounds.height) - inset * 2 - let cellLength = boardLength / 3.0 - - let bounds = view.bounds.inset(by: view.safeAreaInsets) - titleLabel.frame = CGRect( - x: bounds.origin.x, - y: bounds.origin.y, - width: bounds.size.width, - height: 44.0 - ) - - var yOffset = (view.bounds.height - boardLength) / 2.0 - for row in cells { - var xOffset = inset - for cell in row { - cell.frame = CGRect( - x: xOffset, - y: yOffset, - width: cellLength, - height: cellLength - ) - - xOffset += inset + cellLength - } - yOffset += inset + cellLength - } - } - - override func screenDidChange(from previousScreen: GamePlayScreen, previousEnvironment: ViewEnvironment) { - super.screenDidChange(from: previousScreen, previousEnvironment: previousEnvironment) - - let title: String - switch screen.gameState { - case let .ongoing(turn: turn): - switch turn { - case .x: - title = "\(screen.playerX), place your 🙅" - case .o: - title = "\(screen.playerO), place your 🙆" - } - - case .tie: - title = "It's a Tie!" - - case let .win(player): - switch player { - case .x: - title = "The 🙅's have it, \(screen.playerX) wins!" - case .o: - title = "The 🙆's have it, \(screen.playerO) wins!" - } - } - titleLabel.text = title - - for row in 0 ..< 3 { - let cols = screen.board[row] - for col in 0 ..< 3 { - switch cols[col] { - case .empty: - cells[row][col].setTitle("", for: .normal) - case let .taken(player): - switch player { - case .x: - cells[row][col].setTitle("🙅", for: .normal) - case .o: - cells[row][col].setTitle("🙆", for: .normal) - } - } - } - } - } - - @objc private func buttonPressed(sender: UIButton) { - for row in 0 ..< 3 { - let cols = cells[row] - for col in 0 ..< 3 { - if cols[col] == sender { - screen.onSelected(row, col) - return - } - } - } - } -} diff --git a/swift/Samples/TicTacToe/Sources/Game/GameState.swift b/swift/Samples/TicTacToe/Sources/Game/GameState.swift deleted file mode 100644 index e6a0405d0..000000000 --- a/swift/Samples/TicTacToe/Sources/Game/GameState.swift +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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. - */ - -enum GameState: Equatable { - case ongoing(turn: Player) - case win(Player) - case tie - - mutating func toggle() { - switch self { - case let .ongoing(turn: player): - switch player { - case .x: - self = .ongoing(turn: .o) - case .o: - self = .ongoing(turn: .x) - } - default: - break - } - } -} diff --git a/swift/Samples/TicTacToe/Sources/Game/NewGameScreen.swift b/swift/Samples/TicTacToe/Sources/Game/NewGameScreen.swift deleted file mode 100644 index 928144a10..000000000 --- a/swift/Samples/TicTacToe/Sources/Game/NewGameScreen.swift +++ /dev/null @@ -1,148 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 WorkflowUI - -struct NewGameScreen: Screen { - var playerX: String - var playerO: String - var eventHandler: (Event) -> Void - - enum Event { - case playerXChanged(String) - case playerOChanged(String) - case startGame - } - - func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { - return NewGameViewController.description(for: self, environment: environment) - } -} - -final class NewGameViewController: ScreenViewController { - let playerXLabel = UILabel(frame: .zero) - let playerXField = UITextField(frame: .zero) - let playerOLabel = UILabel(frame: .zero) - let playerOField = UITextField(frame: .zero) - let startGameButton = UIButton(frame: .zero) - - override func viewDidLoad() { - super.viewDidLoad() - - playerXLabel.text = "Player X" - playerXField.backgroundColor = UIColor(white: 0.92, alpha: 1.0) - playerXField.addTarget(self, action: #selector(onTextChanged(sender:)), for: .editingChanged) - - playerOLabel.text = "Player O" - playerOField.backgroundColor = UIColor(white: 0.92, alpha: 1.0) - playerOField.addTarget(self, action: #selector(onTextChanged(sender:)), for: .editingChanged) - - startGameButton.backgroundColor = UIColor(red: 41 / 255, green: 150 / 255, blue: 204 / 255, alpha: 1.0) - startGameButton.setTitle("Let's Play!", for: .normal) - startGameButton.addTarget(self, action: #selector(startPressed(sender:)), for: .touchUpInside) - - view.addSubview(playerXLabel) - view.addSubview(playerXField) - view.addSubview(playerOLabel) - view.addSubview(playerOField) - view.addSubview(startGameButton) - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - - let inset: CGFloat = 12.0 - let height: CGFloat = 44.0 - var yOffset = (view.bounds.size.height - (3 * height + inset)) / 2.0 - - let xSize = playerXLabel.sizeThatFits(CGSize( - width: view.bounds.size.width, - height: height - )) - - playerXLabel.frame = CGRect( - x: view.bounds.origin.x, - y: yOffset, - width: xSize.width, - height: height - ) - - playerXField.frame = CGRect( - x: view.bounds.origin.x + xSize.width, - y: yOffset, - width: view.bounds.size.width, - height: height - ) - .insetBy(dx: inset, dy: 0.0) - - yOffset += height + inset - - let oSize = playerOLabel.sizeThatFits(CGSize( - width: view.bounds.size.width, - height: height - )) - - playerOLabel.frame = CGRect( - x: view.bounds.origin.x, - y: yOffset, - width: oSize.width, - height: height - ) - - playerOField.frame = CGRect( - x: view.bounds.origin.x + oSize.width, - y: yOffset, - width: view.bounds.size.width, - height: height - ) - .insetBy(dx: inset, dy: 0.0) - - yOffset += height + inset - - startGameButton.frame = CGRect( - x: view.bounds.origin.x, - y: yOffset, - width: view.bounds.size.width, - height: height - ) - .insetBy(dx: inset, dy: 0.0) - - yOffset += height + inset - } - - override func screenDidChange(from previousScreen: NewGameScreen, previousEnvironment: ViewEnvironment) { - super.screenDidChange(from: previousScreen, previousEnvironment: previousEnvironment) - - playerXField.text = screen.playerX - playerOField.text = screen.playerO - } - - @objc private func onTextChanged(sender: UITextField) { - guard let name = sender.text else { - return - } - - if sender == playerXField { - screen.eventHandler(.playerXChanged(name)) - } else if sender == playerOField { - screen.eventHandler(.playerOChanged(name)) - } - } - - @objc private func startPressed(sender: UIButton) { - screen.eventHandler(.startGame) - } -} diff --git a/swift/Samples/TicTacToe/Sources/Game/RunGameWorkflow.swift b/swift/Samples/TicTacToe/Sources/Game/RunGameWorkflow.swift deleted file mode 100644 index b65a2a6ec..000000000 --- a/swift/Samples/TicTacToe/Sources/Game/RunGameWorkflow.swift +++ /dev/null @@ -1,181 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 AlertContainer -import BackStackContainer -import ModalContainer -import Workflow -import WorkflowUI - -// MARK: Input and Output - -struct RunGameWorkflow: Workflow { - typealias Output = Never -} - -// MARK: State and Initialization - -extension RunGameWorkflow { - struct State: Equatable { - var playerX: String - var playerO: String - var step: Step - - enum Step { - case newGame - case playing - case maybeQuit - } - } - - func makeInitialState() -> RunGameWorkflow.State { - return State(playerX: "X", playerO: "O", step: .newGame) - } -} - -// MARK: Actions - -extension RunGameWorkflow { - enum Action: WorkflowAction { - typealias WorkflowType = RunGameWorkflow - - case updatePlayerX(String) - case updatePlayerO(String) - case startGame - case back - case confirmQuit - - func apply(toState state: inout RunGameWorkflow.State) -> RunGameWorkflow.Output? { - switch self { - case let .updatePlayerX(name): - state.playerX = name - - case let .updatePlayerO(name): - state.playerO = name - - case .startGame: - state.step = .playing - - case .back: - state.step = .newGame - - case .confirmQuit: - state.step = .maybeQuit - } - - return nil - } - } -} - -// MARK: Rendering - -extension RunGameWorkflow { - typealias Rendering = AlertContainerScreen>> - - func render(state: RunGameWorkflow.State, context: RenderContext) -> Rendering { - let sink = context.makeSink(of: Action.self) - var modals: [ModalContainerScreenModal] = [] - var alert: Alert? - - var backStackItems: [BackStackScreen.Item] = [BackStackScreen.Item( - screen: newGameScreen( - sink: sink, - playerX: state.playerX, - playerO: state.playerO - ).asAnyScreen(), - barVisibility: .hidden - )] - - switch state.step { - case .newGame: - break - - case .playing: - let takeTurnsScreen = TakeTurnsWorkflow( - playerX: state.playerX, - playerO: state.playerO - ) - .rendered(with: context) - backStackItems.append(BackStackScreen.Item( - screen: takeTurnsScreen.asAnyScreen(), - barVisibility: .visible(BackStackScreen.BarContent( - leftItem: BackStackScreen.BarContent.BarButtonItem.button(BackStackScreen.BarContent.Button( - content: .text("Quit"), - handler: { - sink.send(.confirmQuit) - } - )) - )) - )) - - case .maybeQuit: - - let takeTurnsScreen = TakeTurnsWorkflow( - playerX: state.playerX, - playerO: state.playerO - ) - .rendered(with: context) - backStackItems.append(BackStackScreen.Item( - screen: takeTurnsScreen.asAnyScreen(), - barVisibility: .visible(BackStackScreen.BarContent( - leftItem: BackStackScreen.BarContent.BarButtonItem.button(BackStackScreen.BarContent.Button( - content: .text("Quit"), - handler: { - sink.send(.confirmQuit) - } - )) - )) - )) - - let (confirmQuitScreen, confirmQuitAlert) = ConfirmQuitWorkflow() - .mapOutput { output -> Action in - switch output { - case .cancel: - return .startGame - case .quit: - return .back - } - } - .rendered(with: context) - alert = confirmQuitAlert - modals.append(ModalContainerScreenModal(screen: AnyScreen(confirmQuitScreen), style: .fullScreen, key: "0", animated: true)) - } - - let modalContainerScreen = ModalContainerScreen(baseScreen: BackStackScreen(items: backStackItems), modals: modals) - - return AlertContainerScreen(baseScreen: modalContainerScreen, alert: alert) - } - - private func newGameScreen(sink: Sink, playerX: String, playerO: String) -> NewGameScreen { - return NewGameScreen( - playerX: playerX, - playerO: playerO, - eventHandler: { event in - switch event { - case .startGame: - sink.send(.startGame) - - case let .playerXChanged(name): - sink.send(.updatePlayerX(name)) - - case let .playerOChanged(name): - sink.send(.updatePlayerO(name)) - } - } - ) - } -} diff --git a/swift/Samples/TicTacToe/Sources/Game/TakeTurnsWorkflow.swift b/swift/Samples/TicTacToe/Sources/Game/TakeTurnsWorkflow.swift deleted file mode 100644 index d372f617c..000000000 --- a/swift/Samples/TicTacToe/Sources/Game/TakeTurnsWorkflow.swift +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 Workflow -import WorkflowUI - -// MARK: Input and Output - -struct TakeTurnsWorkflow: Workflow { - var playerX: String - var playerO: String - - typealias Output = Never -} - -// MARK: State and Initialization - -extension TakeTurnsWorkflow { - struct State: Equatable { - var board: Board - var gameState: GameState - } - - func makeInitialState() -> TakeTurnsWorkflow.State { - return State(board: Board(), gameState: .ongoing(turn: .x)) - } -} - -// MARK: Actions - -extension TakeTurnsWorkflow { - enum Action: WorkflowAction { - typealias WorkflowType = TakeTurnsWorkflow - - case selected(row: Int, col: Int) - - func apply(toState state: inout TakeTurnsWorkflow.State) -> TakeTurnsWorkflow.Output? { - switch state.gameState { - case let .ongoing(turn: turn): - switch self { - case let .selected(row: row, col: col): - if !state.board.isEmpty(row: row, col: col) { - return nil - } - - state.board.takeSquare(row: row, col: col, player: turn) - - if state.board.hasVictory() { - state.gameState = .win(turn) - return nil - } else if state.board.isFull() { - state.gameState = .tie - return nil - } else { - state.gameState.toggle() - return nil - } - } - - case .tie: - return nil - case .win: - return nil - } - } - } -} - -// MARK: Rendering - -extension TakeTurnsWorkflow { - typealias Rendering = GamePlayScreen - - func render(state: TakeTurnsWorkflow.State, context: RenderContext) -> Rendering { - let sink = context.makeSink(of: Action.self) - - return GamePlayScreen( - gameState: state.gameState, - playerX: playerX, - playerO: playerO, - board: state.board.rows, - onSelected: { row, col in - sink.send(.selected(row: row, col: col)) - } - ) - } -} diff --git a/swift/Samples/TicTacToe/Sources/Main/MainWorkflow.swift b/swift/Samples/TicTacToe/Sources/Main/MainWorkflow.swift deleted file mode 100644 index 3ffdef5e4..000000000 --- a/swift/Samples/TicTacToe/Sources/Main/MainWorkflow.swift +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 AlertContainer -import BackStackContainer -import ModalContainer -import Workflow -import WorkflowUI - -// MARK: Input and Output - -struct MainWorkflow: Workflow { - typealias Output = Never -} - -// MARK: State and Initialization - -extension MainWorkflow { - enum State: Equatable { - case authenticating - case runningGame(sessionToken: String) - } - - func makeInitialState() -> MainWorkflow.State { - return .authenticating - } -} - -// MARK: Actions - -extension MainWorkflow { - enum Action: WorkflowAction { - typealias WorkflowType = MainWorkflow - - case authenticated(sessionToken: String) - case logout - - func apply(toState state: inout MainWorkflow.State) -> MainWorkflow.Output? { - switch self { - case let .authenticated(sessionToken: sessionToken): - state = .runningGame(sessionToken: sessionToken) - - case .logout: - state = .authenticating - } - - return nil - } - } -} - -// MARK: Rendering - -extension MainWorkflow { - typealias Rendering = AlertContainerScreen>> - - func render(state: MainWorkflow.State, context: RenderContext) -> Rendering { - switch state { - case .authenticating: - return AuthenticationWorkflow(authenticationService: AuthenticationService()) - .mapOutput { output -> Action in - switch output { - case let .authorized(session: sessionToken): - return .authenticated(sessionToken: sessionToken) - } - } - .rendered(with: context) - - case .runningGame: - return RunGameWorkflow().rendered(with: context) - } - } -} diff --git a/swift/Samples/TicTacToe/Tests/AuthenticationWorkflowTests.swift b/swift/Samples/TicTacToe/Tests/AuthenticationWorkflowTests.swift deleted file mode 100644 index 3de59b071..000000000 --- a/swift/Samples/TicTacToe/Tests/AuthenticationWorkflowTests.swift +++ /dev/null @@ -1,366 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 Workflow -import WorkflowTesting -import XCTest - -@testable import Development_SampleTicTacToe - -class AuthenticationWorkflowTests: XCTestCase { - // MARK: Action Tests - - func test_action_back() { - AuthenticationWorkflow - .Action - .tester(withState: .twoFactor(intermediateSession: "test", authenticationError: nil)) - .send(action: .back) - .assertState { state in - XCTAssertEqual(state, .emailPassword) - } - } - - func test_action_login() { - AuthenticationWorkflow - .Action - .tester(withState: .emailPassword) - .send(action: .login(email: "reza@example.com", password: "password")) - .assertState { state in - if case let .authorizingEmailPassword(email, password) = state { - XCTAssertEqual(email, "reza@example.com") - XCTAssertEqual(password, "password") - } else { - XCTFail("Unexpected emailPassword in state \(state)") - } - } - } - - func test_action_verifySecondFactor() { - AuthenticationWorkflow - .Action - .tester(withState: .emailPassword) - .send( - action: .verifySecondFactor( - intermediateSession: "intermediateSession", - twoFactorCode: "twoFactorCode" - ) - ) - .assertState { state in - if case let .authorizingTwoFactor(twoFactorCode, intermediateSession) = state { - XCTAssertEqual(intermediateSession, "intermediateSession") - XCTAssertEqual(twoFactorCode, "twoFactorCode") - } else { - XCTFail("Unexpected verifySecondFactor in state \(state)") - } - } - } - - func test_action_authenticationSucceeded() { - AuthenticationWorkflow - .Action - .tester(withState: .emailPassword) - .send( - action: .authenticationSucceeded( - response: AuthenticationService.AuthenticationResponse( - token: "token", - secondFactorRequired: true - ) - ) - ) - .assertState { state in - if case let .twoFactor(intermediateSession, authenticationError) = state { - XCTAssertEqual(intermediateSession, "token") - XCTAssertNil(authenticationError) - } else { - XCTFail("Unexpected authenticationSucceeded in state \(state)") - } - } - - AuthenticationWorkflow - .Action - .tester(withState: .emailPassword) - .send( - action: .authenticationSucceeded( - response: AuthenticationService.AuthenticationResponse( - token: "token", - secondFactorRequired: false - ) - ), outputAssertions: { output in - XCTAssertNotNil(output) - switch output! { - case let .authorized(session: session): - XCTAssertEqual(session, "token") - } - } - ) - .assertState { state in - XCTAssertEqual(state, .emailPassword) - } - } - - func test_action_dismissAuthenticationAlert() { - AuthenticationWorkflow - .Action - .tester( - withState: .authorizingEmailPassword( - email: "reza@example.com", - password: "password" - ) - ) - .send( - action: .authenticationError(AuthenticationService.AuthenticationError.invalidUserPassword) - ) - .assertState { state in - if case let .authenticationErrorAlert(error) = state { - XCTAssertNotNil(error) - XCTAssertEqual(error, AuthenticationService.AuthenticationError.invalidUserPassword) - } else { - XCTFail("Unexpected authenticationError in state \(state)") - } - } - .send(action: .dismissAuthenticationAlert) - .assertState { state in - XCTAssertEqual(state, .emailPassword) - } - } - - func test_action_authenticationError() { - AuthenticationWorkflow - .Action - .tester( - withState: .authorizingEmailPassword( - email: "reza@example.com", - password: "password" - ) - ) - .send( - action: .authenticationError(AuthenticationService.AuthenticationError.invalidUserPassword) - ) - .assertState { state in - if case let .authenticationErrorAlert(error) = state { - XCTAssertNotNil(error) - XCTAssertEqual(error, AuthenticationService.AuthenticationError.invalidUserPassword) - } else { - XCTFail("Unexpected authenticationError in state \(state)") - } - } - - AuthenticationWorkflow - .Action - .tester( - withState: .authorizingTwoFactor( - twoFactorCode: "twoFactorCode", - intermediateSession: "intermediateSession" - ) - ) - .send( - action: .authenticationError(AuthenticationService.AuthenticationError.invalidTwoFactor) - ) - .assertState { state in - if case let .twoFactor(intermediateSession, error) = state { - XCTAssertNotNil(intermediateSession) - XCTAssertNotNil(error) - XCTAssertEqual(error, AuthenticationService.AuthenticationError.invalidTwoFactor) - } else { - XCTFail("Unexpected authenticationError in state \(state)") - } - } - } - - // MARK: Render Tests - - func test_render_initial() { - let authenticationWorkFlow = AuthenticationWorkflow(authenticationService: AuthenticationService()) - let expectedState = ExpectedState(state: .emailPassword) - - let expectedWorkflow = ExpectedWorkflow( - type: LoginWorkflow.self, - rendering: LoginScreen( - title: "", - email: "", - onEmailChanged: { _ in }, - password: "", - onPasswordChanged: { _ in }, - onLoginTapped: {} - ), - output: nil - ) - - let renderExpectations = RenderExpectations( - expectedState: expectedState, - expectedOutput: nil, - expectedWorkers: [], - expectedWorkflows: [expectedWorkflow] - ) - - authenticationWorkFlow - .renderTester(initialState: .emailPassword) - .render( - with: renderExpectations, - assertions: { screen in - XCTAssertNil(screen.alert) - } - ) - } - - func test_render_AuthorizingEmailPasswordWorker() { - let authenticationService = AuthenticationService() - let authenticationWorkFlow = AuthenticationWorkflow(authenticationService: authenticationService) - - let expectedState = ExpectedState( - state: .authorizingEmailPassword( - email: "reza@example.com", - password: "password" - ) - ) - - let expectedWorkflow = ExpectedWorkflow( - type: LoginWorkflow.self, - rendering: LoginScreen( - title: "", - email: "", - onEmailChanged: { _ in }, - password: "", - onPasswordChanged: { _ in }, - onLoginTapped: {} - ), - output: nil - ) - - let expectedWorker = ExpectedWorker( - worker: AuthenticationWorkflow.AuthorizingEmailPasswordWorker( - authenticationService: authenticationService, - email: "reza@example.com", - password: "password" - ) - ) - - let renderExpectations = RenderExpectations( - expectedState: expectedState, - expectedOutput: nil, - expectedWorkers: [expectedWorker], - expectedWorkflows: [expectedWorkflow] - ) - - authenticationWorkFlow - .renderTester( - initialState: .authorizingEmailPassword( - email: "reza@example.com", - password: "password" - ) - ) - .render( - with: renderExpectations, - assertions: { screen in - XCTAssertNil(screen.alert) - } - ) - } - - func test_render_authorizingTwoFactorWorker() { - let authenticationService = AuthenticationService() - let authenticationWorkFlow = AuthenticationWorkflow(authenticationService: authenticationService) - - let expectedState = ExpectedState( - state: .authorizingTwoFactor( - twoFactorCode: "twoFactorCode", - intermediateSession: "intermediateSession" - ) - ) - - let expectedWorkflow = ExpectedWorkflow( - type: LoginWorkflow.self, - rendering: LoginScreen( - title: "", - email: "", - onEmailChanged: { _ in }, - password: "", - onPasswordChanged: { _ in }, - onLoginTapped: {} - ), - output: nil - ) - - let expectedWorker = ExpectedWorker( - worker: AuthenticationWorkflow.AuthorizingTwoFactorWorker( - authenticationService: authenticationService, - intermediateToken: "intermediateSession", - twoFactorCode: "twoFactorCode" - ) - ) - - let renderExpectations = RenderExpectations( - expectedState: expectedState, - expectedOutput: nil, - expectedWorkers: [expectedWorker], - expectedWorkflows: [expectedWorkflow] - ) - - authenticationWorkFlow - .renderTester( - initialState: .authorizingTwoFactor( - twoFactorCode: "twoFactorCode", - intermediateSession: "intermediateSession" - ) - ) - .render( - with: renderExpectations, - assertions: { screen in - XCTAssertNil(screen.alert) - } - ) - } - - func test_render_authenticationErrorAlert() { - let authenticationService = AuthenticationService() - let authenticationWorkFlow = AuthenticationWorkflow(authenticationService: authenticationService) - - let expectedState = ExpectedState( - state: .authenticationErrorAlert(error: AuthenticationService.AuthenticationError.invalidUserPassword) - ) - - let expectedWorkflow = ExpectedWorkflow( - type: LoginWorkflow.self, - rendering: LoginScreen( - title: "", - email: "", - onEmailChanged: { _ in }, - password: "", - onPasswordChanged: { _ in }, - onLoginTapped: {} - ), - output: nil - ) - - let renderExpectations = RenderExpectations( - expectedState: expectedState, - expectedOutput: nil, - expectedWorkers: [], - expectedWorkflows: [expectedWorkflow] - ) - - authenticationWorkFlow - .renderTester( - initialState: .authenticationErrorAlert(error: AuthenticationService.AuthenticationError.invalidUserPassword) - ) - .render( - with: renderExpectations, - assertions: { screen in - XCTAssertNotNil(screen.alert) - } - ) - } -} diff --git a/swift/Samples/TicTacToe/Tests/ConfirmQuitWorkflowTests.swift b/swift/Samples/TicTacToe/Tests/ConfirmQuitWorkflowTests.swift deleted file mode 100644 index 25815f821..000000000 --- a/swift/Samples/TicTacToe/Tests/ConfirmQuitWorkflowTests.swift +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 Workflow -import WorkflowTesting -import XCTest - -@testable import Development_SampleTicTacToe - -class ConfirmQuitWorkflowTests: XCTestCase { - // MARK: Action Tests - - func test_action_cancel() { - ConfirmQuitWorkflow - .Action - .tester(withState: ConfirmQuitWorkflow.State(step: .confirmOnce)) - .send( - action: .cancel, - outputAssertions: { output in - XCTAssertEqual(output, ConfirmQuitWorkflow.Output.cancel) - } - ) - } - - func test_action_quit() { - ConfirmQuitWorkflow - .Action - .tester(withState: ConfirmQuitWorkflow.State(step: .confirmOnce)) - .send( - action: .quit, - outputAssertions: { output in - XCTAssertEqual(output, ConfirmQuitWorkflow.Output.quit) - } - ) - } - - func test_action_confirm() { - ConfirmQuitWorkflow - .Action - .tester(withState: ConfirmQuitWorkflow.State(step: .confirmOnce)) - .send(action: .confirm) - .assertState { state in - XCTAssertEqual(state.step, .confirmTwice) - } - } - - // MARK: Render Tests - - func test_render_confirmOnce() { - let confirmQuitWorkflow = ConfirmQuitWorkflow() - confirmQuitWorkflow - .renderTester(initialState: ConfirmQuitWorkflow.State(step: .confirmOnce)) - .render( - assertions: { screen in - XCTAssertNotNil(screen) - XCTAssertNotNil(screen.0) - XCTAssertNil(screen.1) - XCTAssertEqual(screen.0.question, "Are you sure you want to quit?") - } - ) - } - - func test_render_confirmTwice() { - let confirmQuitWorkflow = ConfirmQuitWorkflow() - confirmQuitWorkflow - .renderTester(initialState: ConfirmQuitWorkflow.State(step: .confirmTwice)) - .render( - assertions: { screen in - XCTAssertNotNil(screen) - XCTAssertNotNil(screen.0) - XCTAssertNotNil(screen.1) - XCTAssertEqual(screen.1!.title, "Confirm Again") - XCTAssertEqual(screen.1!.message, "Do you really want to quit?") - XCTAssertEqual(screen.0.question, "Are you sure you want to quit?") - XCTAssertEqual(screen.1!.actions[0].title, "Not really") - XCTAssertEqual(screen.1!.actions[1].title, "Yes, please!") - } - ) - } -} diff --git a/swift/Samples/TicTacToe/Tests/Info.plist b/swift/Samples/TicTacToe/Tests/Info.plist deleted file mode 100644 index 64d65ca49..000000000 --- a/swift/Samples/TicTacToe/Tests/Info.plist +++ /dev/null @@ -1,22 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - $(PRODUCT_BUNDLE_PACKAGE_TYPE) - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - - diff --git a/swift/Samples/TicTacToe/Tests/LoginWorkflowTests.swift b/swift/Samples/TicTacToe/Tests/LoginWorkflowTests.swift deleted file mode 100644 index a36305d22..000000000 --- a/swift/Samples/TicTacToe/Tests/LoginWorkflowTests.swift +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 Workflow -import WorkflowTesting -import XCTest - -@testable import Development_SampleTicTacToe - -class LoginWorkflowTests: XCTestCase { - // MARK: Action Tests - - func test_action_emailUpdate() { - LoginWorkflow - .Action - .tester( - withState: LoginWorkflow.State( - email: "reza@example.com", - password: "password" - ) - ) - .send( - action: .emailUpdated("square@example.com"), - outputAssertions: { output in - XCTAssertNil(output) - } - ) - .assertState { state in - XCTAssertEqual(state.email, "square@example.com") - XCTAssertEqual(state.password, "password") - } - } - - func test_action_passwordUpdate() { - LoginWorkflow - .Action - .tester( - withState: LoginWorkflow.State( - email: "reza@example.com", - password: "password" - ) - ) - .send( - action: .passwordUpdated("drowssap"), - outputAssertions: { output in - XCTAssertNil(output) - } - ) - .assertState { state in - XCTAssertEqual(state.email, "reza@example.com") - XCTAssertEqual(state.password, "drowssap") - } - } - - func test_action_login() { - LoginWorkflow - .Action - .tester( - withState: LoginWorkflow.State( - email: "reza@example.com", - password: "password" - ) - ) - .send( - action: .login, - outputAssertions: { output in - XCTAssertNotNil(output) - switch output! { - case let .login(email, password): - XCTAssertEqual(email, "reza@example.com") - XCTAssertEqual(password, "password") - } - } - ) - } - - // MARK: Render Tests - - func test_render_initial() { - let loginWorkflow = LoginWorkflow() - loginWorkflow - .renderTester(initialState: LoginWorkflow.State(email: "reza@example.com", password: "password")) - .render(assertions: { screen in - XCTAssertEqual(screen.title, "Welcome! Please log in to play TicTacToe!") - XCTAssertEqual(screen.email, "reza@example.com") - XCTAssertEqual(screen.password, "password") - } - ) - } -} diff --git a/swift/Samples/TicTacToe/Tests/MainWorkflowTests.swift b/swift/Samples/TicTacToe/Tests/MainWorkflowTests.swift deleted file mode 100644 index 1833975ab..000000000 --- a/swift/Samples/TicTacToe/Tests/MainWorkflowTests.swift +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 BackStackContainer -import ModalContainer -import Workflow -import WorkflowTesting -import XCTest - -@testable import Development_SampleTicTacToe - -class MainWorkflowTests: XCTestCase { - // MARK: Action Tests - - func test_action_authenticated() { - MainWorkflow - .Action - .tester(withState: .authenticating) - .send(action: .authenticated(sessionToken: "token")) - .assertState { state in - if case let MainWorkflow.State.runningGame(token) = state { - XCTAssertEqual(token, "token") - } else { - XCTFail("Invalid state after authenticated") - } - } - } - - func test_action_logout() { - MainWorkflow - .Action - .tester(withState: .runningGame(sessionToken: "token")) - .send(action: .logout) - .assertState { state in - XCTAssertEqual(state, .authenticating) - } - } - - // MARK: Render Tests - - func test_render_authenticating() { - let mainWorkflow = MainWorkflow() - - let expectedState = ExpectedState(state: .authenticating) - - let expectedWorkflow = ExpectedWorkflow( - type: AuthenticationWorkflow.self, - rendering: AuthenticationWorkflow.Rendering( - baseScreen: ModalContainerScreen( - baseScreen: BackStackScreen(items: []), modals: [] - ), - alert: nil - ) - ) - - let renderExpectations = RenderExpectations( - expectedState: expectedState, - expectedOutput: nil, - expectedWorkers: [], - expectedWorkflows: [expectedWorkflow] - ) - - mainWorkflow - .renderTester() - .render( - with: renderExpectations, - assertions: { screen in - XCTAssertNil(screen.alert) - } - ) - } - - func disabled_test_render_runningGame() { - let mainWorkflow = MainWorkflow() - - let expectedState = ExpectedState(state: .runningGame(sessionToken: "token")) - - let renderExpectations = RenderExpectations( - expectedState: expectedState, - expectedOutput: nil, - expectedWorkers: [], - expectedWorkflows: [] - ) - - mainWorkflow - .renderTester() - .render( - with: renderExpectations, - assertions: { screen in - XCTAssertNil(screen.alert) - } - ) - } -} diff --git a/swift/Samples/TicTacToe/Tests/RunGameWorkflowTests.swift b/swift/Samples/TicTacToe/Tests/RunGameWorkflowTests.swift deleted file mode 100644 index e11ad1b74..000000000 --- a/swift/Samples/TicTacToe/Tests/RunGameWorkflowTests.swift +++ /dev/null @@ -1,229 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 AlertContainer -import Workflow -import WorkflowTesting -import XCTest - -@testable import Development_SampleTicTacToe - -class RunGameWorkflowTests: XCTestCase { - // MARK: Action Tests - - func test_action_updatePlayers() { - let initalState = RunGameWorkflow.State(playerX: "X", playerO: "O", step: .newGame) - - RunGameWorkflow - .Action - .tester(withState: initalState) - .send( - action: .updatePlayerX("❌"), - outputAssertions: { output in - XCTAssertNil(output) - } - ) - .assertState { state in - XCTAssertEqual(state.playerX, "❌") - XCTAssertEqual(state.playerO, "O") - XCTAssertEqual(state.step, .newGame) - }.send( - action: .updatePlayerO("🅾️"), - outputAssertions: { output in - XCTAssertNil(output) - } - ) - .assertState { state in - XCTAssertEqual(state.playerX, "❌") - XCTAssertEqual(state.playerO, "🅾️") - XCTAssertEqual(state.step, .newGame) - } - } - - func test_action_startGame() { - let initalState = RunGameWorkflow.State( - playerX: "X", - playerO: "O", - step: .newGame - ) - - RunGameWorkflow - .Action - .tester(withState: initalState) - .send( - action: .startGame, - outputAssertions: { output in - XCTAssertNil(output) - } - ) - .assertState { state in - XCTAssertEqual(state.playerX, "X") - XCTAssertEqual(state.playerO, "O") - XCTAssertEqual(state.step, .playing) - } - } - - func test_action_back() { - let playingState = RunGameWorkflow.State( - playerX: "X", - playerO: "O", - step: .playing - ) - - RunGameWorkflow - .Action - .tester(withState: playingState) - .send( - action: .back, - outputAssertions: { output in - XCTAssertNil(output) - } - ) - .assertState { state in - XCTAssertEqual(state.playerX, "X") - XCTAssertEqual(state.playerO, "O") - XCTAssertEqual(state.step, .newGame) - } - } - - func test_action_confirmQuit() { - let playingState = RunGameWorkflow.State( - playerX: "X", - playerO: "O", - step: .playing - ) - - RunGameWorkflow - .Action - .tester(withState: playingState) - .send( - action: .confirmQuit, - outputAssertions: { output in - XCTAssertNil(output) - } - ) - .assertState { state in - XCTAssertEqual(state.playerX, "X") - XCTAssertEqual(state.playerO, "O") - XCTAssertEqual(state.step, .maybeQuit) - } - } - - // MARK: Render Tests - - func test_render_newGame() { - let playingState = RunGameWorkflow.State( - playerX: "X", - playerO: "O", - step: .newGame - ) - - let runGameWorkflow = RunGameWorkflow() - - runGameWorkflow - .renderTester(initialState: playingState) - .render( - assertions: { screen in - } - ) - } - - func test_render_playing() { - let playingState = RunGameWorkflow.State( - playerX: "X", - playerO: "O", - step: .playing - ) - - let runGameWorkflow = RunGameWorkflow() - - let expectedState = ExpectedState( - state: RunGameWorkflow.State( - playerX: "X", - playerO: "O", - step: .playing - ) - ) - - let expectedTakeTurnWorkflow = ExpectedWorkflow( - type: TakeTurnsWorkflow.self, - rendering: TakeTurnsWorkflow.Rendering(gameState: .tie, playerX: "", playerO: "", board: [], onSelected: { _, _ in }) - ) - - let expectedRender = RenderExpectations( - expectedState: expectedState, - expectedOutput: nil, - expectedWorkers: [], - expectedWorkflows: [expectedTakeTurnWorkflow] - ) - - runGameWorkflow - .renderTester(initialState: playingState) - .render( - with: expectedRender, - assertions: { screen in - XCTAssertNil(screen.alert) - } - ) - } - - func test_render_maybeQuit() { - let playingState = RunGameWorkflow.State( - playerX: "X", - playerO: "O", - step: .maybeQuit - ) - - let runGameWorkflow = RunGameWorkflow() - - let expectedState = ExpectedState( - state: RunGameWorkflow.State( - playerX: "X", - playerO: "O", - step: .maybeQuit - ) - ) - - let expectedConfirmQuitWorkflow = ExpectedWorkflow( - type: ConfirmQuitWorkflow.self, - rendering: ConfirmQuitWorkflow.Rendering(ConfirmQuitScreen(question: ""), Alert(title: "title", message: "message", actions: [])) - ) - - let expectedTakeTurnWorkflow = ExpectedWorkflow( - type: TakeTurnsWorkflow.self, - rendering: TakeTurnsWorkflow.Rendering(gameState: .tie, playerX: "", playerO: "", board: [], onSelected: { _, _ in }) - ) - - let expectedRender = RenderExpectations( - expectedState: expectedState, - expectedOutput: nil, - expectedWorkers: [], - expectedWorkflows: [expectedConfirmQuitWorkflow, expectedTakeTurnWorkflow] - ) - - runGameWorkflow - .renderTester(initialState: playingState) - .render( - with: expectedRender, - assertions: { screen in - XCTAssertNotNil(screen.alert) - XCTAssertEqual(screen.alert!.title, "title") - XCTAssertEqual(screen.alert!.message, "message") - XCTAssertEqual(screen.baseScreen.modals.count, 1) - } - ) - } -} diff --git a/swift/Samples/TicTacToe/Tests/TakeTurnsWorkflowTests.swift b/swift/Samples/TicTacToe/Tests/TakeTurnsWorkflowTests.swift deleted file mode 100644 index 515b0ab20..000000000 --- a/swift/Samples/TicTacToe/Tests/TakeTurnsWorkflowTests.swift +++ /dev/null @@ -1,309 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 Workflow -import WorkflowTesting -import XCTest - -@testable import Development_SampleTicTacToe - -class TakeTurnsWorkflowTests: XCTestCase { - // MARK: Action Tests - - /* - - _|_|_ X|_|_ - _|_|_ => _|_|_ - _|_|_ _|_|_ - - */ - func test_action_selected_initialMove() { - let emptyBoardState = TakeTurnsWorkflow.State( - board: Board(), - gameState: .ongoing(turn: .x) - ) - - TakeTurnsWorkflow - .Action - .tester(withState: emptyBoardState) - .send( - action: .selected(row: 0, col: 0), - outputAssertions: { output in - // This workflow has no outputs. - XCTAssertNil(output) - } - ) - // After x takes 0, 0 we expect the following state: - .assertState { state in - // Board is not full. - XCTAssertFalse(state.board.isFull()) - - // Cell at 0, 0 is not empty. - XCTAssertFalse(state.board.isEmpty(row: 0, col: 0)) - - // Cell at 0, 0 is taken by player x. - XCTAssertEqual(state.board.rows[0][0], Board.Cell.taken(.x)) - - // We do not have a victory. - XCTAssertFalse(state.board.hasVictory()) - - // Game state is now in ongoing but for player o. - if case GameState.ongoing(turn: .o) = state.gameState { - XCTAssertTrue(true) - } else { - XCTFail("after x takes 0, 0. It should be o's turn") - } - } - /* - - X|_|_ X|_|_ - _|_|_ => _|O|_ - _|_|_ _|_|_ - - */ - .send( - action: .selected(row: 0, col: 1) - // After o takes 0, 1 we expect the following state: - ).assertState { state in - // Board is not full. - XCTAssertFalse(state.board.isFull()) - - // Cell at 0, 0 is not empty. - XCTAssertFalse(state.board.isEmpty(row: 0, col: 0)) - // Cell at 0, 1 is not empty. - XCTAssertFalse(state.board.isEmpty(row: 0, col: 1)) - - // Cell at 0, 0 is taken by player x. - XCTAssertEqual(state.board.rows[0][0], Board.Cell.taken(.x)) - - // Cell at 0, 1 is taken by player x. - XCTAssertEqual(state.board.rows[0][1], Board.Cell.taken(.o)) - - // We do not have a victory. - XCTAssertFalse(state.board.hasVictory()) - - // Game state is now in ongoing but for player 0. - if case GameState.ongoing(turn: .x) = state.gameState { - XCTAssertTrue(true) - } else { - XCTFail("after o takes 0, 1. It should be x's turn") - } - } - } - - /* - - X|O|X X|O|X - O|X|O => O|X|O - O|X|_ O|X|O - - */ - func test_action_selected_tieGame() { - var board = Board() - board.takeSquare(row: 0, col: 0, player: .x) - board.takeSquare(row: 0, col: 1, player: .o) - board.takeSquare(row: 0, col: 2, player: .x) - - board.takeSquare(row: 1, col: 0, player: .o) - board.takeSquare(row: 1, col: 1, player: .x) - board.takeSquare(row: 1, col: 2, player: .o) - - board.takeSquare(row: 2, col: 0, player: .o) - board.takeSquare(row: 2, col: 1, player: .x) - - let boardState = TakeTurnsWorkflow.State( - board: board, - gameState: .ongoing(turn: .o) - ) - - TakeTurnsWorkflow - .Action - .tester(withState: boardState) - .send( - action: .selected(row: 2, col: 2), - outputAssertions: { output in - // This workflow has no outputs. - XCTAssertNil(output) - } - ) - // After o takes 2, 2 we expect the following state: - .assertState { state in - // Board is full - XCTAssertTrue(state.board.isFull()) - - // We do not have a victory. - XCTAssertFalse(state.board.hasVictory()) - - // Game state is now a tie. - if case GameState.tie = state.gameState { - XCTAssertTrue(true) - } else { - XCTFail("when o takes spot 2, 2 we should end up in a tie") - } - } - } - - /* - - X|X|O X|X|O - _|O|O => X|O|O - X|O|X X|O|X - - */ - func test_action_selected_victory() { - var board = Board() - board.takeSquare(row: 0, col: 0, player: .x) - board.takeSquare(row: 0, col: 1, player: .x) - board.takeSquare(row: 0, col: 2, player: .o) - - board.takeSquare(row: 1, col: 1, player: .o) - board.takeSquare(row: 1, col: 2, player: .o) - - board.takeSquare(row: 2, col: 0, player: .x) - board.takeSquare(row: 2, col: 1, player: .o) - board.takeSquare(row: 2, col: 2, player: .x) - - let boardState = TakeTurnsWorkflow.State( - board: board, - gameState: .ongoing(turn: .x) - ) - - TakeTurnsWorkflow - .Action - .tester(withState: boardState) - .send( - action: .selected(row: 1, col: 0), - outputAssertions: { output in - // This workflow has no outputs. - XCTAssertNil(output) - } - ) - // After o takes 2, 2 we expect the following state: - .assertState { state in - // Board is full. - XCTAssertTrue(state.board.isFull()) - - // We do have a victory. - XCTAssertTrue(state.board.hasVictory()) - - // Game state is now in a win for player x. - if case GameState.win(.x) = state.gameState { - XCTAssertTrue(true) - } else { - XCTFail("when x takes spot 1, 0 we should end up in a victory for player x") - } - } - } - - // MARK: Render Tests - - // Empty board with X making the first move. - func test_render_initialBoard() { - let emptyBoardState = TakeTurnsWorkflow.State( - board: Board(), - gameState: .ongoing(turn: .x) - ) - - let expectedState = ExpectedState(state: emptyBoardState) - - let renderExpectation = RenderExpectations( - expectedState: expectedState, - expectedOutput: nil, - expectedWorkers: [], - expectedWorkflows: [] - ) - - let workflow = TakeTurnsWorkflow( - playerX: "X", - playerO: "O" - ) - - workflow - .renderTester() - .render( - with: renderExpectation - ) { screen in - - // The display value for player X should match what was passed to the workflow. - XCTAssertEqual(screen.playerX, "X") - - // The display value for player O should match what was passed to the workflow. - XCTAssertEqual(screen.playerO, "O") - - // The screen state should match with player x going next. - if case GameState.ongoing(turn: .x) = screen.gameState { - XCTAssertTrue(true) - } else { - XCTFail("x should start the game since the board is setup with ongoing(turn: .x)") - } - XCTAssertEqual(screen.board, Board().rows) - } - } - - func test_render_winningBoard() { - var board = Board() - board.takeSquare(row: 0, col: 0, player: .x) - board.takeSquare(row: 0, col: 1, player: .x) - board.takeSquare(row: 0, col: 2, player: .o) - - board.takeSquare(row: 1, col: 0, player: .o) - board.takeSquare(row: 1, col: 1, player: .o) - board.takeSquare(row: 1, col: 2, player: .o) - - board.takeSquare(row: 2, col: 0, player: .x) - board.takeSquare(row: 2, col: 1, player: .o) - board.takeSquare(row: 2, col: 2, player: .x) - - let boardState = TakeTurnsWorkflow.State( - board: board, - gameState: .win(.o) - ) - - let expectedState = ExpectedState(state: boardState) - - let renderExpectation = RenderExpectations( - expectedState: expectedState, - expectedOutput: nil, - expectedWorkers: [], - expectedWorkflows: [] - ) - - let workflow = TakeTurnsWorkflow( - playerX: "X", - playerO: "O" - ) - - workflow - .renderTester(initialState: boardState) - .render( - with: renderExpectation - ) { screen in - - // The display value for player X should match what was passed to the workflow. - XCTAssertEqual(screen.playerX, "X") - - // The display value for player O should match what was passed to the workflow. - XCTAssertEqual(screen.playerO, "O") - - // The screen state should match with player o winning. - if case GameState.win(.o) = screen.gameState { - XCTAssertTrue(true) - } else { - XCTFail("o should win the game") - } - } - } -} diff --git a/swift/Samples/Tutorial/.gitignore b/swift/Samples/Tutorial/.gitignore deleted file mode 100644 index 05ef11923..000000000 --- a/swift/Samples/Tutorial/.gitignore +++ /dev/null @@ -1 +0,0 @@ -Podfile.lock diff --git a/swift/Samples/Tutorial/AppHost/Configuration/Info.plist b/swift/Samples/Tutorial/AppHost/Configuration/Info.plist deleted file mode 100644 index f0b505750..000000000 --- a/swift/Samples/Tutorial/AppHost/Configuration/Info.plist +++ /dev/null @@ -1,47 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - APPL - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - LSRequiresIPhoneOS - - UILaunchStoryboardName - LaunchScreen - CFBundleDisplayName - - LSApplicationCategoryType - - UIRequiredDeviceCapabilities - - armv7 - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - - diff --git a/swift/Samples/Tutorial/AppHost/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/swift/Samples/Tutorial/AppHost/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index d8db8d65f..000000000 --- a/swift/Samples/Tutorial/AppHost/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,98 +0,0 @@ -{ - "images" : [ - { - "idiom" : "iphone", - "size" : "20x20", - "scale" : "2x" - }, - { - "idiom" : "iphone", - "size" : "20x20", - "scale" : "3x" - }, - { - "idiom" : "iphone", - "size" : "29x29", - "scale" : "2x" - }, - { - "idiom" : "iphone", - "size" : "29x29", - "scale" : "3x" - }, - { - "idiom" : "iphone", - "size" : "40x40", - "scale" : "2x" - }, - { - "idiom" : "iphone", - "size" : "40x40", - "scale" : "3x" - }, - { - "idiom" : "iphone", - "size" : "60x60", - "scale" : "2x" - }, - { - "idiom" : "iphone", - "size" : "60x60", - "scale" : "3x" - }, - { - "idiom" : "ipad", - "size" : "20x20", - "scale" : "1x" - }, - { - "idiom" : "ipad", - "size" : "20x20", - "scale" : "2x" - }, - { - "idiom" : "ipad", - "size" : "29x29", - "scale" : "1x" - }, - { - "idiom" : "ipad", - "size" : "29x29", - "scale" : "2x" - }, - { - "idiom" : "ipad", - "size" : "40x40", - "scale" : "1x" - }, - { - "idiom" : "ipad", - "size" : "40x40", - "scale" : "2x" - }, - { - "idiom" : "ipad", - "size" : "76x76", - "scale" : "1x" - }, - { - "idiom" : "ipad", - "size" : "76x76", - "scale" : "2x" - }, - { - "idiom" : "ipad", - "size" : "83.5x83.5", - "scale" : "2x" - }, - { - "idiom" : "ios-marketing", - "size" : "1024x1024", - "scale" : "1x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/swift/Samples/Tutorial/AppHost/Resources/Assets.xcassets/Contents.json b/swift/Samples/Tutorial/AppHost/Resources/Assets.xcassets/Contents.json deleted file mode 100644 index da4a164c9..000000000 --- a/swift/Samples/Tutorial/AppHost/Resources/Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/swift/Samples/Tutorial/AppHost/Resources/Base.lproj/LaunchScreen.storyboard b/swift/Samples/Tutorial/AppHost/Resources/Base.lproj/LaunchScreen.storyboard deleted file mode 100644 index bfa361294..000000000 --- a/swift/Samples/Tutorial/AppHost/Resources/Base.lproj/LaunchScreen.storyboard +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/swift/Samples/Tutorial/AppHost/Sources/AppDelegate.swift b/swift/Samples/Tutorial/AppHost/Sources/AppDelegate.swift deleted file mode 100644 index 84371e622..000000000 --- a/swift/Samples/Tutorial/AppHost/Sources/AppDelegate.swift +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 TutorialBase -import UIKit - -@UIApplicationMain -class AppDelegate: UIResponder, UIApplicationDelegate { - var window: UIWindow? - - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - window = UIWindow(frame: UIScreen.main.bounds) - - let viewController = TutorialContainerViewController() - - window?.rootViewController = viewController - - window?.makeKeyAndVisible() - - return true - } -} diff --git a/swift/Samples/Tutorial/AppHost/TutorialTests/Info.plist b/swift/Samples/Tutorial/AppHost/TutorialTests/Info.plist deleted file mode 100644 index 6c40a6cd0..000000000 --- a/swift/Samples/Tutorial/AppHost/TutorialTests/Info.plist +++ /dev/null @@ -1,22 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - BNDL - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - - diff --git a/swift/Samples/Tutorial/AppHost/TutorialTests/TutorialTests.swift b/swift/Samples/Tutorial/AppHost/TutorialTests/TutorialTests.swift deleted file mode 100644 index 7f496fc2e..000000000 --- a/swift/Samples/Tutorial/AppHost/TutorialTests/TutorialTests.swift +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 XCTest - -class TutorialTests: XCTestCase { - func testExample() {} -} diff --git a/swift/Samples/Tutorial/Frameworks/Tutorial1Complete/Sources/Todo/Edit/TodoEditSampleViewController.swift b/swift/Samples/Tutorial/Frameworks/Tutorial1Complete/Sources/Todo/Edit/TodoEditSampleViewController.swift deleted file mode 100644 index 17e596b7f..000000000 --- a/swift/Samples/Tutorial/Frameworks/Tutorial1Complete/Sources/Todo/Edit/TodoEditSampleViewController.swift +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 TutorialViews - -final class TodoEditSampleViewController: UIViewController { - let todoEditView: TodoEditView - - init() { - self.todoEditView = TodoEditView(frame: .zero) - - super.init(nibName: nil, bundle: nil) - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: UIViewController - - override func viewDidLoad() { - super.viewDidLoad() - - view.addSubview(todoEditView) - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - - todoEditView.frame = view.bounds.inset(by: view.safeAreaInsets) - } -} diff --git a/swift/Samples/Tutorial/Frameworks/Tutorial1Complete/Sources/Todo/List/TodoListSampleViewController.swift b/swift/Samples/Tutorial/Frameworks/Tutorial1Complete/Sources/Todo/List/TodoListSampleViewController.swift deleted file mode 100644 index c0b40e0c1..000000000 --- a/swift/Samples/Tutorial/Frameworks/Tutorial1Complete/Sources/Todo/List/TodoListSampleViewController.swift +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 TutorialViews - -final class TodoListSampleViewController: UIViewController { - let todoListView: TodoListView - - init() { - self.todoListView = TodoListView(frame: .zero) - - super.init(nibName: nil, bundle: nil) - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: UIViewController - - override func viewDidLoad() { - super.viewDidLoad() - - view.addSubview(todoListView) - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - - todoListView.frame = view.bounds.inset(by: view.safeAreaInsets) - } -} diff --git a/swift/Samples/Tutorial/Frameworks/Tutorial1Complete/Sources/Todo/Model/TodoModel.swift b/swift/Samples/Tutorial/Frameworks/Tutorial1Complete/Sources/Todo/Model/TodoModel.swift deleted file mode 100644 index 4aa909e03..000000000 --- a/swift/Samples/Tutorial/Frameworks/Tutorial1Complete/Sources/Todo/Model/TodoModel.swift +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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. - */ - -struct TodoModel: Equatable { - var title: String - var note: String -} diff --git a/swift/Samples/Tutorial/Frameworks/Tutorial1Complete/Sources/TutorialContainerViewController.swift b/swift/Samples/Tutorial/Frameworks/Tutorial1Complete/Sources/TutorialContainerViewController.swift deleted file mode 100644 index 88ecc08ae..000000000 --- a/swift/Samples/Tutorial/Frameworks/Tutorial1Complete/Sources/TutorialContainerViewController.swift +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 UIKit -import Workflow -import WorkflowUI - -public final class TutorialContainerViewController: UIViewController { - let containerViewController: UIViewController - - public init() { - // Create a `ContainerViewController` with the `WelcomeWorkflow` as the root workflow - self.containerViewController = ContainerViewController(workflow: WelcomeWorkflow()) - - super.init(nibName: nil, bundle: nil) - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override public func viewDidLoad() { - super.viewDidLoad() - - view.backgroundColor = .white - - addChild(containerViewController) - view.addSubview(containerViewController.view) - containerViewController.didMove(toParent: self) - } - - override public func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - - containerViewController.view.frame = view.bounds - } -} diff --git a/swift/Samples/Tutorial/Frameworks/Tutorial1Complete/Sources/Welcome/WelcomeScreen.swift b/swift/Samples/Tutorial/Frameworks/Tutorial1Complete/Sources/Welcome/WelcomeScreen.swift deleted file mode 100644 index 474033f3a..000000000 --- a/swift/Samples/Tutorial/Frameworks/Tutorial1Complete/Sources/Welcome/WelcomeScreen.swift +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 TutorialViews -import Workflow -import WorkflowUI - -struct WelcomeScreen: Screen { - /// The current name that has been entered. - var name: String - /// Callback when the name changes in the UI. - var onNameChanged: (String) -> Void - /// Callback when the login button is tapped. - var onLoginTapped: () -> Void - - func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { - return WelcomeViewController.description(for: self, environment: environment) - } -} - -final class WelcomeViewController: ScreenViewController { - var welcomeView: WelcomeView - - required init(screen: WelcomeScreen, environment: ViewEnvironment) { - self.welcomeView = WelcomeView(frame: .zero) - super.init(screen: screen, environment: environment) - update(with: screen) - } - - override func viewDidLoad() { - super.viewDidLoad() - - view.addSubview(welcomeView) - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - - welcomeView.frame = view.bounds.inset(by: view.safeAreaInsets) - } - - override func screenDidChange(from previousScreen: WelcomeScreen, previousEnvironment: ViewEnvironment) { - update(with: screen) - } - - private func update(with screen: WelcomeScreen) { - /// Update UI - welcomeView.name = screen.name - welcomeView.onNameChanged = screen.onNameChanged - welcomeView.onLoginTapped = screen.onLoginTapped - } -} diff --git a/swift/Samples/Tutorial/Frameworks/Tutorial1Complete/Sources/Welcome/WelcomeWorkflow.swift b/swift/Samples/Tutorial/Frameworks/Tutorial1Complete/Sources/Welcome/WelcomeWorkflow.swift deleted file mode 100644 index 62ec4b3d9..000000000 --- a/swift/Samples/Tutorial/Frameworks/Tutorial1Complete/Sources/Welcome/WelcomeWorkflow.swift +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 ReactiveSwift -import Workflow -import WorkflowUI - -// MARK: Input and Output - -struct WelcomeWorkflow: Workflow { - enum Output {} -} - -// MARK: State and Initialization - -extension WelcomeWorkflow { - struct State { - var name: String - } - - func makeInitialState() -> WelcomeWorkflow.State { - return State(name: "") - } - - func workflowDidChange(from previousWorkflow: WelcomeWorkflow, state: inout State) {} -} - -// MARK: Actions - -extension WelcomeWorkflow { - enum Action: WorkflowAction { - typealias WorkflowType = WelcomeWorkflow - - case nameChanged(name: String) - - func apply(toState state: inout WelcomeWorkflow.State) -> WelcomeWorkflow.Output? { - switch self { - case let .nameChanged(name: name): - // Update our state with the updated name. - state.name = name - // Return `nil` for the output, we want to handle this action only at the level of this workflow. - return nil - } - } - } -} - -// MARK: Workers - -extension WelcomeWorkflow { - struct WelcomeWorker: Worker { - enum Output {} - - func run() -> SignalProducer { - fatalError() - } - - func isEquivalent(to otherWorker: WelcomeWorker) -> Bool { - return true - } - } -} - -// MARK: Rendering - -extension WelcomeWorkflow { - typealias Rendering = WelcomeScreen - - func render(state: WelcomeWorkflow.State, context: RenderContext) -> Rendering { - // Create a "sink" of type `Action`. A sink is what we use to send actions to the workflow. - let sink = context.makeSink(of: Action.self) - - return WelcomeScreen( - name: state.name, - onNameChanged: { name in - sink.send(.nameChanged(name: name)) - }, - onLoginTapped: {} - ) - } -} diff --git a/swift/Samples/Tutorial/Frameworks/Tutorial1Complete/Tutorial1.podspec b/swift/Samples/Tutorial/Frameworks/Tutorial1Complete/Tutorial1.podspec deleted file mode 100644 index f9361d3d2..000000000 --- a/swift/Samples/Tutorial/Frameworks/Tutorial1Complete/Tutorial1.podspec +++ /dev/null @@ -1,22 +0,0 @@ -Pod::Spec.new do |s| - s.name = 'Tutorial1' - s.version = '1.0.0.LOCAL' - s.summary = 'See the README.' - s.homepage = 'https://github.com/square/workflow' - s.license = 'Apache License, Version 2.0' - s.author = 'Square' - s.source = { git: 'Not Published', tag: "podify/#{s.version}" } - - # 1.7 is needed for `swift_versions` support - s.cocoapods_version = '>= 1.7.0' - - s.swift_versions = ['5.0'] - s.ios.deployment_target = '11.0' - - s.source_files = 'Sources/**/*.swift' - - s.dependency 'TutorialViews' - s.dependency 'Workflow' - s.dependency 'WorkflowUI' - s.dependency 'BackStackContainer' -end diff --git a/swift/Samples/Tutorial/Frameworks/Tutorial2Complete/Sources/RootWorkflow.swift b/swift/Samples/Tutorial/Frameworks/Tutorial2Complete/Sources/RootWorkflow.swift deleted file mode 100644 index 1d0b50435..000000000 --- a/swift/Samples/Tutorial/Frameworks/Tutorial2Complete/Sources/RootWorkflow.swift +++ /dev/null @@ -1,151 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 BackStackContainer -import ReactiveSwift -import Workflow -import WorkflowUI - -// MARK: Input and Output - -struct RootWorkflow: Workflow { - enum Output {} -} - -// MARK: State and Initialization - -extension RootWorkflow { - // The state is an enum, and can either be on the welcome screen or the todo list. - // When on the todo list, it also includes the name provided on the welcome screen - enum State { - // The welcome screen via the welcome workflow will be shown - case welcome - // The todo list screen via the todo list workflow will be shown. The name will be provided to the todo list. - case todo(name: String) - } - - func makeInitialState() -> RootWorkflow.State { - return .welcome - } - - func workflowDidChange(from previousWorkflow: RootWorkflow, state: inout State) {} -} - -// MARK: Actions - -extension RootWorkflow { - enum Action: WorkflowAction { - typealias WorkflowType = RootWorkflow - - case login(name: String) - case logout - - func apply(toState state: inout RootWorkflow.State) -> RootWorkflow.Output? { - switch self { - case let .login(name: name): - // When the `login` action is received, change the state to `todo`. - state = .todo(name: name) - case .logout: - // Return to the welcome state on logout. - state = .welcome - } - return nil - } - } -} - -// MARK: Workers - -extension RootWorkflow { - struct RootWorker: Worker { - enum Output {} - - func run() -> SignalProducer { - fatalError() - } - - func isEquivalent(to otherWorker: RootWorker) -> Bool { - return true - } - } -} - -// MARK: Rendering - -extension RootWorkflow { - typealias Rendering = BackStackScreen - - func render(state: RootWorkflow.State, context: RenderContext) -> Rendering { - // Create a sink to handle the back action from the TodoListWorkflow to logout. - let sink = context.makeSink(of: Action.self) - - // Our list of back stack items. Will always include the "WelcomeScreen". - var backStackItems: [BackStackScreen.Item] = [] - - let welcomeScreen = WelcomeWorkflow() - .mapOutput { output -> Action in - switch output { - // When `WelcomeWorkflow` emits `didLogin`, turn it into our `login` action. - case let .didLogin(name: name): - return .login(name: name) - } - } - .rendered(with: context) - - let welcomeBackStackItem = BackStackScreen.Item( - key: "welcome", - screen: welcomeScreen.asAnyScreen(), - // Hide the navigation bar. - barVisibility: .hidden - ) - - // Always add the welcome back stack item. - backStackItems.append(welcomeBackStackItem) - - switch state { - // When the state is `.welcome`, defer to the WelcomeWorkflow. - case .welcome: - // We always add the welcome screen to the backstack, so this is a no op. - break - - // When the state is `.todo`, defer to the TodoListWorkflow. - case let .todo(name: name): - - let todoListScreen = TodoListWorkflow() - .rendered(with: context) - - let todoListBackStackItem = BackStackScreen.Item( - key: "todoList", - screen: todoListScreen.asAnyScreen(), - // Specify the title, back button, and right button. - barContent: .init( - title: "Welcome \(name)", - // When `back` is pressed, emit the .logout action to return to the welcome screen. - leftItem: .button(.back(handler: { - sink.send(.logout) - })), - rightItem: .none - ) - ) - - // Add the TodoListScreen to our BackStackItems. - backStackItems.append(todoListBackStackItem) - } - - // Finally, return the BackStackScreen with a list of BackStackScreen.Items - return BackStackScreen(items: backStackItems) - } -} diff --git a/swift/Samples/Tutorial/Frameworks/Tutorial2Complete/Sources/Todo/Edit/TodoEditSampleViewController.swift b/swift/Samples/Tutorial/Frameworks/Tutorial2Complete/Sources/Todo/Edit/TodoEditSampleViewController.swift deleted file mode 100644 index 17e596b7f..000000000 --- a/swift/Samples/Tutorial/Frameworks/Tutorial2Complete/Sources/Todo/Edit/TodoEditSampleViewController.swift +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 TutorialViews - -final class TodoEditSampleViewController: UIViewController { - let todoEditView: TodoEditView - - init() { - self.todoEditView = TodoEditView(frame: .zero) - - super.init(nibName: nil, bundle: nil) - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: UIViewController - - override func viewDidLoad() { - super.viewDidLoad() - - view.addSubview(todoEditView) - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - - todoEditView.frame = view.bounds.inset(by: view.safeAreaInsets) - } -} diff --git a/swift/Samples/Tutorial/Frameworks/Tutorial2Complete/Sources/Todo/List/TodoListScreen.swift b/swift/Samples/Tutorial/Frameworks/Tutorial2Complete/Sources/Todo/List/TodoListScreen.swift deleted file mode 100644 index 7108db7dd..000000000 --- a/swift/Samples/Tutorial/Frameworks/Tutorial2Complete/Sources/Todo/List/TodoListScreen.swift +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 TutorialViews -import Workflow -import WorkflowUI - -struct TodoListScreen: Screen { - // The titles of the todo items - var todoTitles: [String] - - // Callback when a todo is selected - var onTodoSelected: (Int) -> Void - - func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { - return TodoListViewController.description(for: self, environment: environment) - } -} - -final class TodoListViewController: ScreenViewController { - let todoListView: TodoListView - - required init(screen: TodoListScreen, environment: ViewEnvironment) { - self.todoListView = TodoListView(frame: .zero) - super.init(screen: screen, environment: environment) - update(with: screen) - } - - override func viewDidLoad() { - super.viewDidLoad() - - view.addSubview(todoListView) - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - - todoListView.frame = view.bounds.inset(by: view.safeAreaInsets) - } - - override func screenDidChange(from previousScreen: TodoListScreen, previousEnvironment: ViewEnvironment) { - update(with: screen) - } - - private func update(with screen: TodoListScreen) { - // Update the todoList on the view with what the screen provided: - todoListView.todoList = screen.todoTitles - todoListView.onTodoSelected = screen.onTodoSelected - } -} diff --git a/swift/Samples/Tutorial/Frameworks/Tutorial2Complete/Sources/Todo/List/TodoListWorkflow.swift b/swift/Samples/Tutorial/Frameworks/Tutorial2Complete/Sources/Todo/List/TodoListWorkflow.swift deleted file mode 100644 index 4185a3a29..000000000 --- a/swift/Samples/Tutorial/Frameworks/Tutorial2Complete/Sources/Todo/List/TodoListWorkflow.swift +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 ReactiveSwift -import Workflow -import WorkflowUI - -// MARK: Input and Output - -struct TodoListWorkflow: Workflow { - typealias Output = Never -} - -// MARK: State and Initialization - -extension TodoListWorkflow { - struct State { - var todos: [TodoModel] - } - - func makeInitialState() -> TodoListWorkflow.State { - return State(todos: [TodoModel(title: "Take the cat for a walk", note: "Cats really need their outside sunshine time. Don't forget to walk Charlie. Hamilton is less excited about the prospect.")]) - } - - func workflowDidChange(from previousWorkflow: TodoListWorkflow, state: inout State) {} -} - -// MARK: Actions - -extension TodoListWorkflow { - enum Action: WorkflowAction { - typealias WorkflowType = TodoListWorkflow - - func apply(toState state: inout TodoListWorkflow.State) -> TodoListWorkflow.Output? { - switch self { - // Update state and produce an optional output based on which action was received. - } - } - } -} - -// MARK: Workers - -extension TodoListWorkflow { - struct TodoListWorker: Worker { - enum Output {} - - func run() -> SignalProducer { - fatalError() - } - - func isEquivalent(to otherWorker: TodoListWorker) -> Bool { - return true - } - } -} - -// MARK: Rendering - -extension TodoListWorkflow { - typealias Rendering = TodoListScreen - - func render(state: TodoListWorkflow.State, context: RenderContext) -> Rendering { - let titles = state.todos.map { (todoModel) -> String in - todoModel.title - } - return TodoListScreen( - todoTitles: titles, - onTodoSelected: { _ in } - ) - } -} diff --git a/swift/Samples/Tutorial/Frameworks/Tutorial2Complete/Sources/Todo/Model/TodoModel.swift b/swift/Samples/Tutorial/Frameworks/Tutorial2Complete/Sources/Todo/Model/TodoModel.swift deleted file mode 100644 index 4aa909e03..000000000 --- a/swift/Samples/Tutorial/Frameworks/Tutorial2Complete/Sources/Todo/Model/TodoModel.swift +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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. - */ - -struct TodoModel: Equatable { - var title: String - var note: String -} diff --git a/swift/Samples/Tutorial/Frameworks/Tutorial2Complete/Sources/TutorialContainerViewController.swift b/swift/Samples/Tutorial/Frameworks/Tutorial2Complete/Sources/TutorialContainerViewController.swift deleted file mode 100644 index 2a8770166..000000000 --- a/swift/Samples/Tutorial/Frameworks/Tutorial2Complete/Sources/TutorialContainerViewController.swift +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 BackStackContainer -import UIKit -import Workflow -import WorkflowUI - -public final class TutorialContainerViewController: UIViewController { - let containerViewController: UIViewController - - public init() { - // Create a `ContainerViewController` with the `RootWorkflow` as the root workflow - self.containerViewController = ContainerViewController(workflow: RootWorkflow()) - - super.init(nibName: nil, bundle: nil) - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override public func viewDidLoad() { - super.viewDidLoad() - - view.backgroundColor = .white - - addChild(containerViewController) - view.addSubview(containerViewController.view) - containerViewController.didMove(toParent: self) - } - - override public func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - - containerViewController.view.frame = view.bounds - } -} diff --git a/swift/Samples/Tutorial/Frameworks/Tutorial2Complete/Sources/Welcome/WelcomeScreen.swift b/swift/Samples/Tutorial/Frameworks/Tutorial2Complete/Sources/Welcome/WelcomeScreen.swift deleted file mode 100644 index 474033f3a..000000000 --- a/swift/Samples/Tutorial/Frameworks/Tutorial2Complete/Sources/Welcome/WelcomeScreen.swift +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 TutorialViews -import Workflow -import WorkflowUI - -struct WelcomeScreen: Screen { - /// The current name that has been entered. - var name: String - /// Callback when the name changes in the UI. - var onNameChanged: (String) -> Void - /// Callback when the login button is tapped. - var onLoginTapped: () -> Void - - func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { - return WelcomeViewController.description(for: self, environment: environment) - } -} - -final class WelcomeViewController: ScreenViewController { - var welcomeView: WelcomeView - - required init(screen: WelcomeScreen, environment: ViewEnvironment) { - self.welcomeView = WelcomeView(frame: .zero) - super.init(screen: screen, environment: environment) - update(with: screen) - } - - override func viewDidLoad() { - super.viewDidLoad() - - view.addSubview(welcomeView) - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - - welcomeView.frame = view.bounds.inset(by: view.safeAreaInsets) - } - - override func screenDidChange(from previousScreen: WelcomeScreen, previousEnvironment: ViewEnvironment) { - update(with: screen) - } - - private func update(with screen: WelcomeScreen) { - /// Update UI - welcomeView.name = screen.name - welcomeView.onNameChanged = screen.onNameChanged - welcomeView.onLoginTapped = screen.onLoginTapped - } -} diff --git a/swift/Samples/Tutorial/Frameworks/Tutorial2Complete/Sources/Welcome/WelcomeWorkflow.swift b/swift/Samples/Tutorial/Frameworks/Tutorial2Complete/Sources/Welcome/WelcomeWorkflow.swift deleted file mode 100644 index aaa2dbf04..000000000 --- a/swift/Samples/Tutorial/Frameworks/Tutorial2Complete/Sources/Welcome/WelcomeWorkflow.swift +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 ReactiveSwift -import Workflow -import WorkflowUI - -// MARK: Input and Output - -struct WelcomeWorkflow: Workflow { - enum Output { - case didLogin(name: String) - } -} - -// MARK: State and Initialization - -extension WelcomeWorkflow { - struct State { - var name: String - } - - func makeInitialState() -> WelcomeWorkflow.State { - return State(name: "") - } - - func workflowDidChange(from previousWorkflow: WelcomeWorkflow, state: inout State) {} -} - -// MARK: Actions - -extension WelcomeWorkflow { - enum Action: WorkflowAction { - typealias WorkflowType = WelcomeWorkflow - - case nameChanged(name: String) - case didLogin - - func apply(toState state: inout WelcomeWorkflow.State) -> WelcomeWorkflow.Output? { - switch self { - case let .nameChanged(name: name): - // Update our state with the updated name. - state.name = name - // Return `nil` for the output, we want to handle this action only at the level of this workflow. - return nil - - case .didLogin: - // Return an output of `didLogin` with the name. - return .didLogin(name: state.name) - } - } - } -} - -// MARK: Workers - -extension WelcomeWorkflow { - struct WelcomeWorker: Worker { - enum Output {} - - func run() -> SignalProducer { - fatalError() - } - - func isEquivalent(to otherWorker: WelcomeWorker) -> Bool { - return true - } - } -} - -// MARK: Rendering - -extension WelcomeWorkflow { - typealias Rendering = WelcomeScreen - - func render(state: WelcomeWorkflow.State, context: RenderContext) -> Rendering { - // Create a "sink" of type `Action`. A sink is what we use to send actions to the workflow. - let sink = context.makeSink(of: Action.self) - - return WelcomeScreen( - name: state.name, - onNameChanged: { name in - sink.send(.nameChanged(name: name)) - }, - onLoginTapped: { - // Whenever the login button is tapped, emit the `.didLogin` action. - sink.send(.didLogin) - } - ) - } -} diff --git a/swift/Samples/Tutorial/Frameworks/Tutorial2Complete/Tutorial2.podspec b/swift/Samples/Tutorial/Frameworks/Tutorial2Complete/Tutorial2.podspec deleted file mode 100644 index c10043b3e..000000000 --- a/swift/Samples/Tutorial/Frameworks/Tutorial2Complete/Tutorial2.podspec +++ /dev/null @@ -1,22 +0,0 @@ -Pod::Spec.new do |s| - s.name = 'Tutorial2' - s.version = '1.0.0.LOCAL' - s.summary = 'See the README.' - s.homepage = 'https://github.com/square/workflow' - s.license = 'Apache License, Version 2.0' - s.author = 'Square' - s.source = { git: 'Not Published', tag: "podify/#{s.version}" } - - # 1.7 is needed for `swift_versions` support - s.cocoapods_version = '>= 1.7.0' - - s.swift_versions = ['5.0'] - s.ios.deployment_target = '11.0' - - s.source_files = 'Sources/**/*.swift' - - s.dependency 'TutorialViews' - s.dependency 'Workflow' - s.dependency 'WorkflowUI' - s.dependency 'BackStackContainer' -end diff --git a/swift/Samples/Tutorial/Frameworks/Tutorial3Complete/Sources/RootWorkflow.swift b/swift/Samples/Tutorial/Frameworks/Tutorial3Complete/Sources/RootWorkflow.swift deleted file mode 100644 index afb039a3e..000000000 --- a/swift/Samples/Tutorial/Frameworks/Tutorial3Complete/Sources/RootWorkflow.swift +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 BackStackContainer -import ReactiveSwift -import Workflow -import WorkflowUI - -// MARK: Input and Output - -struct RootWorkflow: Workflow { - enum Output {} -} - -// MARK: State and Initialization - -extension RootWorkflow { - // The state is an enum, and can either be on the welcome screen or the todo list. - // When on the todo list, it also includes the name provided on the welcome screen - enum State { - // The welcome screen via the welcome workflow will be shown - case welcome - // The todo list screen via the todo list workflow will be shown. The name will be provided to the todo list. - case todo(name: String) - } - - func makeInitialState() -> RootWorkflow.State { - return .welcome - } - - func workflowDidChange(from previousWorkflow: RootWorkflow, state: inout State) {} -} - -// MARK: Actions - -extension RootWorkflow { - enum Action: WorkflowAction { - typealias WorkflowType = RootWorkflow - - case login(name: String) - case logout - - func apply(toState state: inout RootWorkflow.State) -> RootWorkflow.Output? { - switch self { - case let .login(name: name): - // When the `login` action is received, change the state to `todo`. - state = .todo(name: name) - case .logout: - // Return to the welcome state on logout. - state = .welcome - } - return nil - } - } -} - -// MARK: Workers - -extension RootWorkflow { - struct RootWorker: Worker { - enum Output {} - - func run() -> SignalProducer { - fatalError() - } - - func isEquivalent(to otherWorker: RootWorker) -> Bool { - return true - } - } -} - -// MARK: Rendering - -extension RootWorkflow { - typealias Rendering = BackStackScreen - - func render(state: RootWorkflow.State, context: RenderContext) -> Rendering { - // Delete the `let sink = context.makeSink(of: ...) as we no longer need a sink. - - // Our list of back stack items. Will always include the "WelcomeScreen". - var backStackItems: [BackStackScreen.Item] = [] - - let welcomeScreen = WelcomeWorkflow() - .mapOutput { output -> Action in - switch output { - // When `WelcomeWorkflow` emits `didLogin`, turn it into our `login` action. - case let .didLogin(name: name): - return .login(name: name) - } - } - .rendered(with: context) - - let welcomeBackStackItem = BackStackScreen.Item( - key: "welcome", - screen: welcomeScreen.asAnyScreen(), - // Hide the navigation bar. - barVisibility: .hidden - ) - - // Always add the welcome back stack item. - backStackItems.append(welcomeBackStackItem) - - switch state { - // When the state is `.welcome`, defer to the WelcomeWorkflow. - case .welcome: - // We always add the welcome screen to the backstack, so this is a no op. - break - - // When the state is `.todo`, defer to the TodoListWorkflow. - case let .todo(name: name): - - let todoBackStackItems = TodoListWorkflow(name: name) - .mapOutput { output -> Action in - switch output { - case .back: - // When receiving a `.back` output, treat it as a `.logout` action. - return .logout - } - } - .rendered(with: context) - - // Add the todoBackStackItems to our BackStackItems. - backStackItems.append(contentsOf: todoBackStackItems) - } - - // Finally, return the BackStackScreen with a list of BackStackScreen.Items - return BackStackScreen(items: backStackItems) - } -} diff --git a/swift/Samples/Tutorial/Frameworks/Tutorial3Complete/Sources/Todo/Edit/TodoEditScreen.swift b/swift/Samples/Tutorial/Frameworks/Tutorial3Complete/Sources/Todo/Edit/TodoEditScreen.swift deleted file mode 100644 index ec737a423..000000000 --- a/swift/Samples/Tutorial/Frameworks/Tutorial3Complete/Sources/Todo/Edit/TodoEditScreen.swift +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 TutorialViews -import Workflow -import WorkflowUI - -struct TodoEditScreen: Screen { - // The title of this todo item. - var title: String - // The contents, or "note" of the todo. - var note: String - - // Callback for when the title or note changes - var onTitleChanged: (String) -> Void - var onNoteChanged: (String) -> Void - - func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { - return TodoEditViewController.description(for: self, environment: environment) - } -} - -final class TodoEditViewController: ScreenViewController { - // The `todoEditView` has all the logic for displaying the todo and editing. - let todoEditView: TodoEditView - - required init(screen: TodoEditScreen, environment: ViewEnvironment) { - self.todoEditView = TodoEditView(frame: .zero) - - super.init(screen: screen, environment: environment) - update(with: screen) - } - - override func viewDidLoad() { - super.viewDidLoad() - - view.addSubview(todoEditView) - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - - todoEditView.frame = view.bounds.inset(by: view.safeAreaInsets) - } - - override func screenDidChange(from previousScreen: TodoEditScreen, previousEnvironment: ViewEnvironment) { - update(with: screen) - } - - private func update(with screen: TodoEditScreen) { - // Update the view with the data from the screen. - todoEditView.title = screen.title - todoEditView.note = screen.note - todoEditView.onTitleChanged = screen.onTitleChanged - todoEditView.onNoteChanged = screen.onNoteChanged - } -} diff --git a/swift/Samples/Tutorial/Frameworks/Tutorial3Complete/Sources/Todo/Edit/TodoEditWorkflow.swift b/swift/Samples/Tutorial/Frameworks/Tutorial3Complete/Sources/Todo/Edit/TodoEditWorkflow.swift deleted file mode 100644 index 2eb97716e..000000000 --- a/swift/Samples/Tutorial/Frameworks/Tutorial3Complete/Sources/Todo/Edit/TodoEditWorkflow.swift +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 BackStackContainer -import ReactiveSwift -import Workflow -import WorkflowUI - -// MARK: Input and Output - -struct TodoEditWorkflow: Workflow { - // The "Todo" passed from our parent. - var initialTodo: TodoModel - - enum Output { - case discard - case save(TodoModel) - } -} - -// MARK: State and Initialization - -extension TodoEditWorkflow { - struct State { - // The workflow's copy of the Todo item. Changes are local to this workflow. - var todo: TodoModel - } - - func makeInitialState() -> TodoEditWorkflow.State { - return State(todo: initialTodo) - } - - func workflowDidChange(from previousWorkflow: TodoEditWorkflow, state: inout State) { - // The `Todo` from our parent changed. Update our internal copy so we are starting from the same item. - // The "correct" behavior depends on the business logic - would we only want to update if the - // users hasn't changed the todo from the initial one? Or is it ok to delete whatever edits - // were in progress if the state from the parent changes? - if previousWorkflow.initialTodo != initialTodo { - state.todo = initialTodo - } - } -} - -// MARK: Actions - -extension TodoEditWorkflow { - enum Action: WorkflowAction { - typealias WorkflowType = TodoEditWorkflow - - case titleChanged(String) - case noteChanged(String) - case discardChanges - case saveChanges - - func apply(toState state: inout TodoEditWorkflow.State) -> TodoEditWorkflow.Output? { - switch self { - case let .titleChanged(title): - state.todo.title = title - - case let .noteChanged(note): - state.todo.note = note - - case .discardChanges: - // Return the .discard output when the discard action is received. - return .discard - - case .saveChanges: - // Return the .save output with the current todo state when the save action is received. - return .save(state.todo) - } - - return nil - } - } -} - -// MARK: Workers - -extension TodoEditWorkflow { - struct TodoEditWorker: Worker { - enum Output {} - - func run() -> SignalProducer { - fatalError() - } - - func isEquivalent(to otherWorker: TodoEditWorker) -> Bool { - return true - } - } -} - -// MARK: Rendering - -extension TodoEditWorkflow { - typealias Rendering = BackStackScreen.Item - - func render(state: TodoEditWorkflow.State, context: RenderContext) -> Rendering { - // The sink is used to send actions back to this workflow. - let sink = context.makeSink(of: Action.self) - - let todoEditScreen = TodoEditScreen( - title: state.todo.title, - note: state.todo.note, - onTitleChanged: { title in - sink.send(.titleChanged(title)) - }, - onNoteChanged: { note in - sink.send(.noteChanged(note)) - } - ) - - let backStackItem = BackStackScreen.Item( - key: "edit", - screen: todoEditScreen.asAnyScreen(), - barContent: .init( - title: "Edit", - leftItem: .button(.back(handler: { - sink.send(.discardChanges) - })), - rightItem: .button(.init( - content: .text("Save"), - handler: { - sink.send(.saveChanges) - } - )) - ) - ) - return backStackItem - } -} diff --git a/swift/Samples/Tutorial/Frameworks/Tutorial3Complete/Sources/Todo/List/TodoListScreen.swift b/swift/Samples/Tutorial/Frameworks/Tutorial3Complete/Sources/Todo/List/TodoListScreen.swift deleted file mode 100644 index 7108db7dd..000000000 --- a/swift/Samples/Tutorial/Frameworks/Tutorial3Complete/Sources/Todo/List/TodoListScreen.swift +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 TutorialViews -import Workflow -import WorkflowUI - -struct TodoListScreen: Screen { - // The titles of the todo items - var todoTitles: [String] - - // Callback when a todo is selected - var onTodoSelected: (Int) -> Void - - func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { - return TodoListViewController.description(for: self, environment: environment) - } -} - -final class TodoListViewController: ScreenViewController { - let todoListView: TodoListView - - required init(screen: TodoListScreen, environment: ViewEnvironment) { - self.todoListView = TodoListView(frame: .zero) - super.init(screen: screen, environment: environment) - update(with: screen) - } - - override func viewDidLoad() { - super.viewDidLoad() - - view.addSubview(todoListView) - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - - todoListView.frame = view.bounds.inset(by: view.safeAreaInsets) - } - - override func screenDidChange(from previousScreen: TodoListScreen, previousEnvironment: ViewEnvironment) { - update(with: screen) - } - - private func update(with screen: TodoListScreen) { - // Update the todoList on the view with what the screen provided: - todoListView.todoList = screen.todoTitles - todoListView.onTodoSelected = screen.onTodoSelected - } -} diff --git a/swift/Samples/Tutorial/Frameworks/Tutorial3Complete/Sources/Todo/List/TodoListWorkflow.swift b/swift/Samples/Tutorial/Frameworks/Tutorial3Complete/Sources/Todo/List/TodoListWorkflow.swift deleted file mode 100644 index 37ff73d8e..000000000 --- a/swift/Samples/Tutorial/Frameworks/Tutorial3Complete/Sources/Todo/List/TodoListWorkflow.swift +++ /dev/null @@ -1,174 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 BackStackContainer -import ReactiveSwift -import Workflow -import WorkflowUI - -// MARK: Input and Output - -struct TodoListWorkflow: Workflow { - // The name is an input. - var name: String - - enum Output { - case back - } -} - -// MARK: State and Initialization - -extension TodoListWorkflow { - struct State { - var todos: [TodoModel] - var step: Step - enum Step { - // Showing the list of todo items. - case list - // Editing a single item. The state holds the index so it can be updated when a save action is received. - case edit(index: Int) - } - } - - func makeInitialState() -> TodoListWorkflow.State { - return State( - todos: [ - TodoModel( - title: "Take the cat for a walk", - note: "Cats really need their outside sunshine time. Don't forget to walk Charlie. Hamilton is less excited about the prospect." - ), - ], - step: .list - ) - } - - func workflowDidChange(from previousWorkflow: TodoListWorkflow, state: inout State) {} -} - -// MARK: Actions - -extension TodoListWorkflow { - enum Action: WorkflowAction { - typealias WorkflowType = TodoListWorkflow - - case onBack - case selectTodo(index: Int) - case discardChanges - case saveChanges(todo: TodoModel, index: Int) - - func apply(toState state: inout TodoListWorkflow.State) -> TodoListWorkflow.Output? { - switch self { - case .onBack: - // When a `.onBack` action is received, emit a `.back` output - return .back - - case let .selectTodo(index: index): - // When a todo item is selected, edit it. - state.step = .edit(index: index) - return nil - - case .discardChanges: - // When a discard action is received, return to the list. - state.step = .list - return nil - - case let .saveChanges(todo: todo, index: index): - // When changes are saved, update the state of that `todo` item and return to the list. - state.todos[index] = todo - - state.step = .list - return nil - } - } - } -} - -// MARK: Workers - -extension TodoListWorkflow { - struct TodoListWorker: Worker { - enum Output {} - - func run() -> SignalProducer { - fatalError() - } - - func isEquivalent(to otherWorker: TodoListWorker) -> Bool { - return true - } - } -} - -// MARK: Rendering - -extension TodoListWorkflow { - typealias Rendering = [BackStackScreen.Item] - - func render(state: TodoListWorkflow.State, context: RenderContext) -> Rendering { - // Define a sink to be able to send actions. - let sink = context.makeSink(of: Action.self) - - let titles = state.todos.map { (todoModel) -> String in - todoModel.title - } - let todoListScreen = TodoListScreen( - todoTitles: titles, - onTodoSelected: { index in - // Send the `selectTodo` action when a todo is selected in the UI. - sink.send(.selectTodo(index: index)) - } - ) - - let todoListItem = BackStackScreen.Item( - key: "list", - screen: todoListScreen.asAnyScreen(), - barContent: .init( - title: "Welcome \(name)", - leftItem: .button(.back(handler: { - // When the left button is tapped, send the .onBack action. - sink.send(.onBack) - })), - rightItem: .none - ) - ) - - switch state.step { - case .list: - // On the "list" step, return just the list screen. - return [todoListItem] - - case let .edit(index: index): - // On the "edit" step, return both the list and edit screens. - let todoEditItem = TodoEditWorkflow( - initialTodo: state.todos[index]) - .mapOutput { output -> Action in - switch output { - case .discard: - // Send the discardChanges action when the discard output is received. - return .discardChanges - - case let .save(todo): - // Send the saveChanges action when the save output is received. - return .saveChanges(todo: todo, index: index) - } - } - .rendered(with: context) - - return [todoListItem, todoEditItem] - } - } -} diff --git a/swift/Samples/Tutorial/Frameworks/Tutorial3Complete/Sources/Todo/Model/TodoModel.swift b/swift/Samples/Tutorial/Frameworks/Tutorial3Complete/Sources/Todo/Model/TodoModel.swift deleted file mode 100644 index 4aa909e03..000000000 --- a/swift/Samples/Tutorial/Frameworks/Tutorial3Complete/Sources/Todo/Model/TodoModel.swift +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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. - */ - -struct TodoModel: Equatable { - var title: String - var note: String -} diff --git a/swift/Samples/Tutorial/Frameworks/Tutorial3Complete/Sources/TutorialContainerViewController.swift b/swift/Samples/Tutorial/Frameworks/Tutorial3Complete/Sources/TutorialContainerViewController.swift deleted file mode 100644 index 2a8770166..000000000 --- a/swift/Samples/Tutorial/Frameworks/Tutorial3Complete/Sources/TutorialContainerViewController.swift +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 BackStackContainer -import UIKit -import Workflow -import WorkflowUI - -public final class TutorialContainerViewController: UIViewController { - let containerViewController: UIViewController - - public init() { - // Create a `ContainerViewController` with the `RootWorkflow` as the root workflow - self.containerViewController = ContainerViewController(workflow: RootWorkflow()) - - super.init(nibName: nil, bundle: nil) - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override public func viewDidLoad() { - super.viewDidLoad() - - view.backgroundColor = .white - - addChild(containerViewController) - view.addSubview(containerViewController.view) - containerViewController.didMove(toParent: self) - } - - override public func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - - containerViewController.view.frame = view.bounds - } -} diff --git a/swift/Samples/Tutorial/Frameworks/Tutorial3Complete/Sources/Welcome/WelcomeScreen.swift b/swift/Samples/Tutorial/Frameworks/Tutorial3Complete/Sources/Welcome/WelcomeScreen.swift deleted file mode 100644 index 474033f3a..000000000 --- a/swift/Samples/Tutorial/Frameworks/Tutorial3Complete/Sources/Welcome/WelcomeScreen.swift +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 TutorialViews -import Workflow -import WorkflowUI - -struct WelcomeScreen: Screen { - /// The current name that has been entered. - var name: String - /// Callback when the name changes in the UI. - var onNameChanged: (String) -> Void - /// Callback when the login button is tapped. - var onLoginTapped: () -> Void - - func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { - return WelcomeViewController.description(for: self, environment: environment) - } -} - -final class WelcomeViewController: ScreenViewController { - var welcomeView: WelcomeView - - required init(screen: WelcomeScreen, environment: ViewEnvironment) { - self.welcomeView = WelcomeView(frame: .zero) - super.init(screen: screen, environment: environment) - update(with: screen) - } - - override func viewDidLoad() { - super.viewDidLoad() - - view.addSubview(welcomeView) - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - - welcomeView.frame = view.bounds.inset(by: view.safeAreaInsets) - } - - override func screenDidChange(from previousScreen: WelcomeScreen, previousEnvironment: ViewEnvironment) { - update(with: screen) - } - - private func update(with screen: WelcomeScreen) { - /// Update UI - welcomeView.name = screen.name - welcomeView.onNameChanged = screen.onNameChanged - welcomeView.onLoginTapped = screen.onLoginTapped - } -} diff --git a/swift/Samples/Tutorial/Frameworks/Tutorial3Complete/Sources/Welcome/WelcomeWorkflow.swift b/swift/Samples/Tutorial/Frameworks/Tutorial3Complete/Sources/Welcome/WelcomeWorkflow.swift deleted file mode 100644 index aaa2dbf04..000000000 --- a/swift/Samples/Tutorial/Frameworks/Tutorial3Complete/Sources/Welcome/WelcomeWorkflow.swift +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 ReactiveSwift -import Workflow -import WorkflowUI - -// MARK: Input and Output - -struct WelcomeWorkflow: Workflow { - enum Output { - case didLogin(name: String) - } -} - -// MARK: State and Initialization - -extension WelcomeWorkflow { - struct State { - var name: String - } - - func makeInitialState() -> WelcomeWorkflow.State { - return State(name: "") - } - - func workflowDidChange(from previousWorkflow: WelcomeWorkflow, state: inout State) {} -} - -// MARK: Actions - -extension WelcomeWorkflow { - enum Action: WorkflowAction { - typealias WorkflowType = WelcomeWorkflow - - case nameChanged(name: String) - case didLogin - - func apply(toState state: inout WelcomeWorkflow.State) -> WelcomeWorkflow.Output? { - switch self { - case let .nameChanged(name: name): - // Update our state with the updated name. - state.name = name - // Return `nil` for the output, we want to handle this action only at the level of this workflow. - return nil - - case .didLogin: - // Return an output of `didLogin` with the name. - return .didLogin(name: state.name) - } - } - } -} - -// MARK: Workers - -extension WelcomeWorkflow { - struct WelcomeWorker: Worker { - enum Output {} - - func run() -> SignalProducer { - fatalError() - } - - func isEquivalent(to otherWorker: WelcomeWorker) -> Bool { - return true - } - } -} - -// MARK: Rendering - -extension WelcomeWorkflow { - typealias Rendering = WelcomeScreen - - func render(state: WelcomeWorkflow.State, context: RenderContext) -> Rendering { - // Create a "sink" of type `Action`. A sink is what we use to send actions to the workflow. - let sink = context.makeSink(of: Action.self) - - return WelcomeScreen( - name: state.name, - onNameChanged: { name in - sink.send(.nameChanged(name: name)) - }, - onLoginTapped: { - // Whenever the login button is tapped, emit the `.didLogin` action. - sink.send(.didLogin) - } - ) - } -} diff --git a/swift/Samples/Tutorial/Frameworks/Tutorial3Complete/Tutorial3.podspec b/swift/Samples/Tutorial/Frameworks/Tutorial3Complete/Tutorial3.podspec deleted file mode 100644 index 6fe7dea7c..000000000 --- a/swift/Samples/Tutorial/Frameworks/Tutorial3Complete/Tutorial3.podspec +++ /dev/null @@ -1,23 +0,0 @@ -Pod::Spec.new do |s| - s.name = 'Tutorial3' - s.version = '1.0.0.LOCAL' - s.summary = 'See the README.' - s.homepage = 'https://github.com/square/workflow' - s.license = 'Apache License, Version 2.0' - s.author = 'Square' - s.source = { git: 'Not Published', tag: "podify/#{s.version}" } - - # 1.7 is needed for `swift_versions` support - s.cocoapods_version = '>= 1.7.0' - - s.swift_versions = ['5.0'] - s.ios.deployment_target = '11.0' - - s.source_files = 'Sources/**/*.swift' - s.resource_bundle = { 'TutorialResources' => ['Resources/**/*'] } - - s.dependency 'TutorialViews' - s.dependency 'Workflow' - s.dependency 'WorkflowUI' - s.dependency 'BackStackContainer' -end diff --git a/swift/Samples/Tutorial/Frameworks/Tutorial4Complete/Sources/RootWorkflow.swift b/swift/Samples/Tutorial/Frameworks/Tutorial4Complete/Sources/RootWorkflow.swift deleted file mode 100644 index 4138da8b2..000000000 --- a/swift/Samples/Tutorial/Frameworks/Tutorial4Complete/Sources/RootWorkflow.swift +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 BackStackContainer -import ReactiveSwift -import Workflow -import WorkflowUI - -// MARK: Input and Output - -struct RootWorkflow: Workflow { - enum Output {} -} - -// MARK: State and Initialization - -extension RootWorkflow { - // The state is an enum, and can either be on the welcome screen or the todo list. - // When on the todo list, it also includes the name provided on the welcome screen - enum State { - // The welcome screen via the welcome workflow will be shown - case welcome - // The todo list screen via the todo list workflow will be shown. The name will be provided to the todo list. - case todo(name: String) - } - - func makeInitialState() -> RootWorkflow.State { - return .welcome - } - - func workflowDidChange(from previousWorkflow: RootWorkflow, state: inout State) {} -} - -// MARK: Actions - -extension RootWorkflow { - enum Action: WorkflowAction { - typealias WorkflowType = RootWorkflow - - case login(name: String) - case logout - - func apply(toState state: inout RootWorkflow.State) -> RootWorkflow.Output? { - switch self { - case let .login(name: name): - // When the `login` action is received, change the state to `todo`. - state = .todo(name: name) - case .logout: - // Return to the welcome state on logout. - state = .welcome - } - return nil - } - } -} - -// MARK: Workers - -extension RootWorkflow { - struct RootWorker: Worker { - enum Output {} - - func run() -> SignalProducer { - fatalError() - } - - func isEquivalent(to otherWorker: RootWorker) -> Bool { - return true - } - } -} - -// MARK: Rendering - -extension RootWorkflow { - typealias Rendering = BackStackScreen - - func render(state: RootWorkflow.State, context: RenderContext) -> Rendering { - // Delete the `let sink = context.makeSink(of: ...) as we no longer need a sink. - - // Our list of back stack items. Will always include the "WelcomeScreen". - var backStackItems: [BackStackScreen.Item] = [] - - let welcomeScreen = WelcomeWorkflow() - .mapOutput { output -> Action in - switch output { - // When `WelcomeWorkflow` emits `didLogin`, turn it into our `login` action. - case let .didLogin(name: name): - return .login(name: name) - } - } - .rendered(with: context) - - let welcomeBackStackItem = BackStackScreen.Item( - key: "welcome", - screen: welcomeScreen.asAnyScreen(), - // Hide the navigation bar. - barVisibility: .hidden - ) - - // Always add the welcome back stack item. - backStackItems.append(welcomeBackStackItem) - - switch state { - // When the state is `.welcome`, defer to the WelcomeWorkflow. - case .welcome: - // We always add the welcome screen to the backstack, so this is a no op. - break - - // When the state is `.todo`, defer to the TodoListWorkflow. - case let .todo(name: name): - - // was: let todoBackStackItems = TodoListWorkflow(name: name) - let todoBackStackItems = TodoWorkflow(name: name) - .mapOutput { output -> Action in - switch output { - case .back: - // When receiving a `.back` output, treat it as a `.logout` action. - return .logout - } - } - .rendered(with: context) - - backStackItems.append(contentsOf: todoBackStackItems) - } - - // Finally, return the BackStackScreen with a list of BackStackScreen.Items - return BackStackScreen(items: backStackItems) - } -} diff --git a/swift/Samples/Tutorial/Frameworks/Tutorial4Complete/Sources/Todo/Edit/TodoEditScreen.swift b/swift/Samples/Tutorial/Frameworks/Tutorial4Complete/Sources/Todo/Edit/TodoEditScreen.swift deleted file mode 100644 index ec737a423..000000000 --- a/swift/Samples/Tutorial/Frameworks/Tutorial4Complete/Sources/Todo/Edit/TodoEditScreen.swift +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 TutorialViews -import Workflow -import WorkflowUI - -struct TodoEditScreen: Screen { - // The title of this todo item. - var title: String - // The contents, or "note" of the todo. - var note: String - - // Callback for when the title or note changes - var onTitleChanged: (String) -> Void - var onNoteChanged: (String) -> Void - - func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { - return TodoEditViewController.description(for: self, environment: environment) - } -} - -final class TodoEditViewController: ScreenViewController { - // The `todoEditView` has all the logic for displaying the todo and editing. - let todoEditView: TodoEditView - - required init(screen: TodoEditScreen, environment: ViewEnvironment) { - self.todoEditView = TodoEditView(frame: .zero) - - super.init(screen: screen, environment: environment) - update(with: screen) - } - - override func viewDidLoad() { - super.viewDidLoad() - - view.addSubview(todoEditView) - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - - todoEditView.frame = view.bounds.inset(by: view.safeAreaInsets) - } - - override func screenDidChange(from previousScreen: TodoEditScreen, previousEnvironment: ViewEnvironment) { - update(with: screen) - } - - private func update(with screen: TodoEditScreen) { - // Update the view with the data from the screen. - todoEditView.title = screen.title - todoEditView.note = screen.note - todoEditView.onTitleChanged = screen.onTitleChanged - todoEditView.onNoteChanged = screen.onNoteChanged - } -} diff --git a/swift/Samples/Tutorial/Frameworks/Tutorial4Complete/Sources/Todo/Edit/TodoEditWorkflow.swift b/swift/Samples/Tutorial/Frameworks/Tutorial4Complete/Sources/Todo/Edit/TodoEditWorkflow.swift deleted file mode 100644 index 2eb97716e..000000000 --- a/swift/Samples/Tutorial/Frameworks/Tutorial4Complete/Sources/Todo/Edit/TodoEditWorkflow.swift +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 BackStackContainer -import ReactiveSwift -import Workflow -import WorkflowUI - -// MARK: Input and Output - -struct TodoEditWorkflow: Workflow { - // The "Todo" passed from our parent. - var initialTodo: TodoModel - - enum Output { - case discard - case save(TodoModel) - } -} - -// MARK: State and Initialization - -extension TodoEditWorkflow { - struct State { - // The workflow's copy of the Todo item. Changes are local to this workflow. - var todo: TodoModel - } - - func makeInitialState() -> TodoEditWorkflow.State { - return State(todo: initialTodo) - } - - func workflowDidChange(from previousWorkflow: TodoEditWorkflow, state: inout State) { - // The `Todo` from our parent changed. Update our internal copy so we are starting from the same item. - // The "correct" behavior depends on the business logic - would we only want to update if the - // users hasn't changed the todo from the initial one? Or is it ok to delete whatever edits - // were in progress if the state from the parent changes? - if previousWorkflow.initialTodo != initialTodo { - state.todo = initialTodo - } - } -} - -// MARK: Actions - -extension TodoEditWorkflow { - enum Action: WorkflowAction { - typealias WorkflowType = TodoEditWorkflow - - case titleChanged(String) - case noteChanged(String) - case discardChanges - case saveChanges - - func apply(toState state: inout TodoEditWorkflow.State) -> TodoEditWorkflow.Output? { - switch self { - case let .titleChanged(title): - state.todo.title = title - - case let .noteChanged(note): - state.todo.note = note - - case .discardChanges: - // Return the .discard output when the discard action is received. - return .discard - - case .saveChanges: - // Return the .save output with the current todo state when the save action is received. - return .save(state.todo) - } - - return nil - } - } -} - -// MARK: Workers - -extension TodoEditWorkflow { - struct TodoEditWorker: Worker { - enum Output {} - - func run() -> SignalProducer { - fatalError() - } - - func isEquivalent(to otherWorker: TodoEditWorker) -> Bool { - return true - } - } -} - -// MARK: Rendering - -extension TodoEditWorkflow { - typealias Rendering = BackStackScreen.Item - - func render(state: TodoEditWorkflow.State, context: RenderContext) -> Rendering { - // The sink is used to send actions back to this workflow. - let sink = context.makeSink(of: Action.self) - - let todoEditScreen = TodoEditScreen( - title: state.todo.title, - note: state.todo.note, - onTitleChanged: { title in - sink.send(.titleChanged(title)) - }, - onNoteChanged: { note in - sink.send(.noteChanged(note)) - } - ) - - let backStackItem = BackStackScreen.Item( - key: "edit", - screen: todoEditScreen.asAnyScreen(), - barContent: .init( - title: "Edit", - leftItem: .button(.back(handler: { - sink.send(.discardChanges) - })), - rightItem: .button(.init( - content: .text("Save"), - handler: { - sink.send(.saveChanges) - } - )) - ) - ) - return backStackItem - } -} diff --git a/swift/Samples/Tutorial/Frameworks/Tutorial4Complete/Sources/Todo/List/TodoListScreen.swift b/swift/Samples/Tutorial/Frameworks/Tutorial4Complete/Sources/Todo/List/TodoListScreen.swift deleted file mode 100644 index 7108db7dd..000000000 --- a/swift/Samples/Tutorial/Frameworks/Tutorial4Complete/Sources/Todo/List/TodoListScreen.swift +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 TutorialViews -import Workflow -import WorkflowUI - -struct TodoListScreen: Screen { - // The titles of the todo items - var todoTitles: [String] - - // Callback when a todo is selected - var onTodoSelected: (Int) -> Void - - func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { - return TodoListViewController.description(for: self, environment: environment) - } -} - -final class TodoListViewController: ScreenViewController { - let todoListView: TodoListView - - required init(screen: TodoListScreen, environment: ViewEnvironment) { - self.todoListView = TodoListView(frame: .zero) - super.init(screen: screen, environment: environment) - update(with: screen) - } - - override func viewDidLoad() { - super.viewDidLoad() - - view.addSubview(todoListView) - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - - todoListView.frame = view.bounds.inset(by: view.safeAreaInsets) - } - - override func screenDidChange(from previousScreen: TodoListScreen, previousEnvironment: ViewEnvironment) { - update(with: screen) - } - - private func update(with screen: TodoListScreen) { - // Update the todoList on the view with what the screen provided: - todoListView.todoList = screen.todoTitles - todoListView.onTodoSelected = screen.onTodoSelected - } -} diff --git a/swift/Samples/Tutorial/Frameworks/Tutorial4Complete/Sources/Todo/List/TodoListWorkflow.swift b/swift/Samples/Tutorial/Frameworks/Tutorial4Complete/Sources/Todo/List/TodoListWorkflow.swift deleted file mode 100644 index 3331c9bc9..000000000 --- a/swift/Samples/Tutorial/Frameworks/Tutorial4Complete/Sources/Todo/List/TodoListWorkflow.swift +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 BackStackContainer -import ReactiveSwift -import Workflow -import WorkflowUI - -// MARK: Input and Output - -struct TodoListWorkflow: Workflow { - // The name is an input. - var name: String - // Use the list of todo items passed from our parent. - var todos: [TodoModel] - - enum Output { - case back - case selectTodo(index: Int) - case newTodo - } -} - -// MARK: State and Initialization - -extension TodoListWorkflow { - struct State {} - - func makeInitialState() -> TodoListWorkflow.State { - return State() - } - - func workflowDidChange(from previousWorkflow: TodoListWorkflow, state: inout State) {} -} - -// MARK: Actions - -extension TodoListWorkflow { - enum Action: WorkflowAction { - typealias WorkflowType = TodoListWorkflow - - case onBack - case selectTodo(index: Int) - case new - - func apply(toState state: inout TodoListWorkflow.State) -> TodoListWorkflow.Output? { - switch self { - case .onBack: - // When a `.onBack` action is received, emit a `.back` output - return .back - - case let .selectTodo(index: index): - // Tell our parent that a todo item was selected. - return .selectTodo(index: index) - - case .new: - // Tell our parent a new todo item should be created. - return .newTodo - } - } - } -} - -// MARK: Workers - -extension TodoListWorkflow { - struct TodoListWorker: Worker { - enum Output {} - - func run() -> SignalProducer { - fatalError() - } - - func isEquivalent(to otherWorker: TodoListWorker) -> Bool { - return true - } - } -} - -// MARK: Rendering - -extension TodoListWorkflow { - typealias Rendering = BackStackScreen.Item - - func render(state: TodoListWorkflow.State, context: RenderContext) -> Rendering { - // Define a sink to be able to send the .onBack action. - let sink = context.makeSink(of: Action.self) - - let titles = todos.map { (todoModel) -> String in - todoModel.title - } - let todoListScreen = TodoListScreen( - todoTitles: titles, - onTodoSelected: { index in - // Send the `selectTodo` action when a todo is selected in the UI. - sink.send(.selectTodo(index: index)) - } - ) - - let todoListItem = BackStackScreen.Item( - key: "list", - screen: todoListScreen.asAnyScreen(), - barContent: .init( - title: "Welcome \(name)", - leftItem: .button(.back(handler: { - // When the left button is tapped, send the .onBack action. - sink.send(.onBack) - })), - rightItem: .button(.init( - content: .text("New Todo"), - handler: { - sink.send(.new) - } - )) - ) - ) - - return todoListItem - } -} diff --git a/swift/Samples/Tutorial/Frameworks/Tutorial4Complete/Sources/Todo/Model/TodoModel.swift b/swift/Samples/Tutorial/Frameworks/Tutorial4Complete/Sources/Todo/Model/TodoModel.swift deleted file mode 100644 index 4aa909e03..000000000 --- a/swift/Samples/Tutorial/Frameworks/Tutorial4Complete/Sources/Todo/Model/TodoModel.swift +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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. - */ - -struct TodoModel: Equatable { - var title: String - var note: String -} diff --git a/swift/Samples/Tutorial/Frameworks/Tutorial4Complete/Sources/Todo/TodoWorkflow.swift b/swift/Samples/Tutorial/Frameworks/Tutorial4Complete/Sources/Todo/TodoWorkflow.swift deleted file mode 100644 index 4f37bb485..000000000 --- a/swift/Samples/Tutorial/Frameworks/Tutorial4Complete/Sources/Todo/TodoWorkflow.swift +++ /dev/null @@ -1,181 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 BackStackContainer -import ReactiveSwift -import Workflow -import WorkflowUI - -// MARK: Input and Output - -struct TodoWorkflow: Workflow { - var name: String - - enum Output { - case back - } -} - -// MARK: State and Initialization - -extension TodoWorkflow { - struct State { - var todos: [TodoModel] - var step: Step - enum Step { - // Showing the list of todo items. - case list - // Editing a single item. The state holds the index so it can be updated when a save action is received. - case edit(index: Int) - } - } - - func makeInitialState() -> TodoWorkflow.State { - return State( - todos: [ - TodoModel( - title: "Take the cat for a walk", - note: "Cats really need their outside sunshine time. Don't forget to walk Charlie. Hamilton is less excited about the prospect." - ), - ], - step: .list - ) - } - - func workflowDidChange(from previousWorkflow: TodoWorkflow, state: inout State) {} -} - -// MARK: Actions - -extension TodoWorkflow { - enum ListAction: WorkflowAction { - typealias WorkflowType = TodoWorkflow - - case back - case editTodo(index: Int) - case newTodo - - func apply(toState state: inout TodoWorkflow.State) -> TodoWorkflow.Output? { - switch self { - case .back: - return .back - - case let .editTodo(index: index): - state.step = .edit(index: index) - - case .newTodo: - // Append a new todo model to the end of the list. - state.todos.append(TodoModel( - title: "New Todo", - note: "" - )) - } - - return nil - } - } - - enum EditAction: WorkflowAction { - typealias WorkflowType = TodoWorkflow - - case discardChanges - case saveChanges(index: Int, todo: TodoModel) - - func apply(toState state: inout TodoWorkflow.State) -> TodoWorkflow.Output? { - guard case .edit = state.step else { - fatalError("Received edit action when state was not `.edit`.") - } - - switch self { - case .discardChanges: - state.step = .list - - case let .saveChanges(index: index, todo: updatedTodo): - state.todos[index] = updatedTodo - } - // Return to the list view for either a discard or save action. - state.step = .list - - return nil - } - } -} - -// MARK: Workers - -extension TodoWorkflow { - struct TodoWorker: Worker { - enum Output {} - - func run() -> SignalProducer { - fatalError() - } - - func isEquivalent(to otherWorker: TodoWorker) -> Bool { - return true - } - } -} - -// MARK: Rendering - -extension TodoWorkflow { - typealias Rendering = [BackStackScreen.Item] - - func render(state: TodoWorkflow.State, context: RenderContext) -> Rendering { - let todoListItem = TodoListWorkflow( - name: name, - todos: state.todos - ) - .mapOutput { output -> ListAction in - switch output { - case .back: - return .back - - case let .selectTodo(index: index): - return .editTodo(index: index) - - case .newTodo: - return .newTodo - } - } - .rendered(with: context) - - switch state.step { - case .list: - // Return only the list item. - return [todoListItem] - - case let .edit(index: index): - - let todoEditItem = TodoEditWorkflow( - initialTodo: state.todos[index]) - .mapOutput { output -> EditAction in - switch output { - case .discard: - return .discardChanges - - case let .save(updatedTodo): - return .saveChanges(index: index, todo: updatedTodo) - } - } - .rendered(with: context) - - // Return both the list item and edit. - return [todoListItem, todoEditItem] - } - } -} diff --git a/swift/Samples/Tutorial/Frameworks/Tutorial4Complete/Sources/TutorialContainerViewController.swift b/swift/Samples/Tutorial/Frameworks/Tutorial4Complete/Sources/TutorialContainerViewController.swift deleted file mode 100644 index 2a8770166..000000000 --- a/swift/Samples/Tutorial/Frameworks/Tutorial4Complete/Sources/TutorialContainerViewController.swift +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 BackStackContainer -import UIKit -import Workflow -import WorkflowUI - -public final class TutorialContainerViewController: UIViewController { - let containerViewController: UIViewController - - public init() { - // Create a `ContainerViewController` with the `RootWorkflow` as the root workflow - self.containerViewController = ContainerViewController(workflow: RootWorkflow()) - - super.init(nibName: nil, bundle: nil) - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override public func viewDidLoad() { - super.viewDidLoad() - - view.backgroundColor = .white - - addChild(containerViewController) - view.addSubview(containerViewController.view) - containerViewController.didMove(toParent: self) - } - - override public func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - - containerViewController.view.frame = view.bounds - } -} diff --git a/swift/Samples/Tutorial/Frameworks/Tutorial4Complete/Sources/Welcome/WelcomeScreen.swift b/swift/Samples/Tutorial/Frameworks/Tutorial4Complete/Sources/Welcome/WelcomeScreen.swift deleted file mode 100644 index 474033f3a..000000000 --- a/swift/Samples/Tutorial/Frameworks/Tutorial4Complete/Sources/Welcome/WelcomeScreen.swift +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 TutorialViews -import Workflow -import WorkflowUI - -struct WelcomeScreen: Screen { - /// The current name that has been entered. - var name: String - /// Callback when the name changes in the UI. - var onNameChanged: (String) -> Void - /// Callback when the login button is tapped. - var onLoginTapped: () -> Void - - func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { - return WelcomeViewController.description(for: self, environment: environment) - } -} - -final class WelcomeViewController: ScreenViewController { - var welcomeView: WelcomeView - - required init(screen: WelcomeScreen, environment: ViewEnvironment) { - self.welcomeView = WelcomeView(frame: .zero) - super.init(screen: screen, environment: environment) - update(with: screen) - } - - override func viewDidLoad() { - super.viewDidLoad() - - view.addSubview(welcomeView) - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - - welcomeView.frame = view.bounds.inset(by: view.safeAreaInsets) - } - - override func screenDidChange(from previousScreen: WelcomeScreen, previousEnvironment: ViewEnvironment) { - update(with: screen) - } - - private func update(with screen: WelcomeScreen) { - /// Update UI - welcomeView.name = screen.name - welcomeView.onNameChanged = screen.onNameChanged - welcomeView.onLoginTapped = screen.onLoginTapped - } -} diff --git a/swift/Samples/Tutorial/Frameworks/Tutorial4Complete/Sources/Welcome/WelcomeWorkflow.swift b/swift/Samples/Tutorial/Frameworks/Tutorial4Complete/Sources/Welcome/WelcomeWorkflow.swift deleted file mode 100644 index aaa2dbf04..000000000 --- a/swift/Samples/Tutorial/Frameworks/Tutorial4Complete/Sources/Welcome/WelcomeWorkflow.swift +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 ReactiveSwift -import Workflow -import WorkflowUI - -// MARK: Input and Output - -struct WelcomeWorkflow: Workflow { - enum Output { - case didLogin(name: String) - } -} - -// MARK: State and Initialization - -extension WelcomeWorkflow { - struct State { - var name: String - } - - func makeInitialState() -> WelcomeWorkflow.State { - return State(name: "") - } - - func workflowDidChange(from previousWorkflow: WelcomeWorkflow, state: inout State) {} -} - -// MARK: Actions - -extension WelcomeWorkflow { - enum Action: WorkflowAction { - typealias WorkflowType = WelcomeWorkflow - - case nameChanged(name: String) - case didLogin - - func apply(toState state: inout WelcomeWorkflow.State) -> WelcomeWorkflow.Output? { - switch self { - case let .nameChanged(name: name): - // Update our state with the updated name. - state.name = name - // Return `nil` for the output, we want to handle this action only at the level of this workflow. - return nil - - case .didLogin: - // Return an output of `didLogin` with the name. - return .didLogin(name: state.name) - } - } - } -} - -// MARK: Workers - -extension WelcomeWorkflow { - struct WelcomeWorker: Worker { - enum Output {} - - func run() -> SignalProducer { - fatalError() - } - - func isEquivalent(to otherWorker: WelcomeWorker) -> Bool { - return true - } - } -} - -// MARK: Rendering - -extension WelcomeWorkflow { - typealias Rendering = WelcomeScreen - - func render(state: WelcomeWorkflow.State, context: RenderContext) -> Rendering { - // Create a "sink" of type `Action`. A sink is what we use to send actions to the workflow. - let sink = context.makeSink(of: Action.self) - - return WelcomeScreen( - name: state.name, - onNameChanged: { name in - sink.send(.nameChanged(name: name)) - }, - onLoginTapped: { - // Whenever the login button is tapped, emit the `.didLogin` action. - sink.send(.didLogin) - } - ) - } -} diff --git a/swift/Samples/Tutorial/Frameworks/Tutorial4Complete/Tests/TutorialTests.swift b/swift/Samples/Tutorial/Frameworks/Tutorial4Complete/Tests/TutorialTests.swift deleted file mode 100644 index f5b45e2a2..000000000 --- a/swift/Samples/Tutorial/Frameworks/Tutorial4Complete/Tests/TutorialTests.swift +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 XCTest - -final class TutorialTests: XCTestCase { - func testPlaceholder() { - XCTAssertEqual(1, 1, "Placeholder test") - } -} diff --git a/swift/Samples/Tutorial/Frameworks/Tutorial4Complete/Tutorial4.podspec b/swift/Samples/Tutorial/Frameworks/Tutorial4Complete/Tutorial4.podspec deleted file mode 100644 index 6dc5ec539..000000000 --- a/swift/Samples/Tutorial/Frameworks/Tutorial4Complete/Tutorial4.podspec +++ /dev/null @@ -1,29 +0,0 @@ -Pod::Spec.new do |s| - s.name = 'Tutorial4' - s.version = '1.0.0.LOCAL' - s.summary = 'See the README.' - s.homepage = 'https://github.com/square/workflow' - s.license = 'Apache License, Version 2.0' - s.author = 'Square' - s.source = { git: 'Not Published', tag: "podify/#{s.version}" } - - # 1.7 is needed for `swift_versions` support - s.cocoapods_version = '>= 1.7.0' - - s.swift_versions = ['5.0'] - s.ios.deployment_target = '11.0' - - s.source_files = 'Sources/**/*.swift' - s.resource_bundle = { 'TutorialResources' => ['Resources/**/*'] } - - s.dependency 'TutorialViews' - s.dependency 'Workflow' - s.dependency 'WorkflowUI' - s.dependency 'BackStackContainer' - - s.test_spec 'Tests' do |test_spec| - test_spec.source_files = 'Tests/**/*.swift' - - test_spec.framework = 'XCTest' - end -end diff --git a/swift/Samples/Tutorial/Frameworks/Tutorial5Complete/Sources/RootWorkflow.swift b/swift/Samples/Tutorial/Frameworks/Tutorial5Complete/Sources/RootWorkflow.swift deleted file mode 100644 index 44ca506e3..000000000 --- a/swift/Samples/Tutorial/Frameworks/Tutorial5Complete/Sources/RootWorkflow.swift +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 BackStackContainer -import ReactiveSwift -import Workflow -import WorkflowUI - -// MARK: Input and Output - -struct RootWorkflow: Workflow { - enum Output {} -} - -// MARK: State and Initialization - -extension RootWorkflow { - // The state is an enum, and can either be on the welcome screen or the todo list. - // When on the todo list, it also includes the name provided on the welcome screen - enum State: Equatable { - // The welcome screen via the welcome workflow will be shown - case welcome - // The todo list screen via the todo list workflow will be shown. The name will be provided to the todo list. - case todo(name: String) - } - - func makeInitialState() -> RootWorkflow.State { - return .welcome - } - - func workflowDidChange(from previousWorkflow: RootWorkflow, state: inout State) {} -} - -// MARK: Actions - -extension RootWorkflow { - enum Action: WorkflowAction { - typealias WorkflowType = RootWorkflow - - case login(name: String) - case logout - - func apply(toState state: inout RootWorkflow.State) -> RootWorkflow.Output? { - switch self { - case let .login(name: name): - // When the `login` action is received, change the state to `todo`. - state = .todo(name: name) - case .logout: - // Return to the welcome state on logout. - state = .welcome - } - return nil - } - } -} - -// MARK: Workers - -extension RootWorkflow { - struct RootWorker: Worker { - enum Output {} - - func run() -> SignalProducer { - fatalError() - } - - func isEquivalent(to otherWorker: RootWorker) -> Bool { - return true - } - } -} - -// MARK: Rendering - -extension RootWorkflow { - typealias Rendering = BackStackScreen - - func render(state: RootWorkflow.State, context: RenderContext) -> Rendering { - // Delete the `let sink = context.makeSink(of: ...) as we no longer need a sink. - - // Our list of back stack items. Will always include the "WelcomeScreen". - var backStackItems: [BackStackScreen.Item] = [] - - let welcomeScreen = WelcomeWorkflow() - .mapOutput { output -> Action in - switch output { - // When `WelcomeWorkflow` emits `didLogin`, turn it into our `login` action. - case let .didLogin(name: name): - return .login(name: name) - } - } - .rendered(with: context) - - let welcomeBackStackItem = BackStackScreen.Item( - key: "welcome", - screen: welcomeScreen.asAnyScreen(), - // Hide the navigation bar. - barVisibility: .hidden - ) - - // Always add the welcome back stack item. - backStackItems.append(welcomeBackStackItem) - - switch state { - // When the state is `.welcome`, defer to the WelcomeWorkflow. - case .welcome: - // We always add the welcome screen to the backstack, so this is a no op. - break - - // When the state is `.todo`, defer to the TodoListWorkflow. - case let .todo(name: name): - - // was: let todoBackStackItems = TodoListWorkflow(name: name) - let todoBackStackItems = TodoWorkflow(name: name) - .mapOutput { output -> Action in - switch output { - case .back: - // When receiving a `.back` output, treat it as a `.logout` action. - return .logout - } - } - .rendered(with: context) - - backStackItems.append(contentsOf: todoBackStackItems) - } - - // Finally, return the BackStackScreen with a list of BackStackScreen.Items - return BackStackScreen(items: backStackItems) - } -} diff --git a/swift/Samples/Tutorial/Frameworks/Tutorial5Complete/Sources/Todo/Edit/TodoEditScreen.swift b/swift/Samples/Tutorial/Frameworks/Tutorial5Complete/Sources/Todo/Edit/TodoEditScreen.swift deleted file mode 100644 index ec737a423..000000000 --- a/swift/Samples/Tutorial/Frameworks/Tutorial5Complete/Sources/Todo/Edit/TodoEditScreen.swift +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 TutorialViews -import Workflow -import WorkflowUI - -struct TodoEditScreen: Screen { - // The title of this todo item. - var title: String - // The contents, or "note" of the todo. - var note: String - - // Callback for when the title or note changes - var onTitleChanged: (String) -> Void - var onNoteChanged: (String) -> Void - - func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { - return TodoEditViewController.description(for: self, environment: environment) - } -} - -final class TodoEditViewController: ScreenViewController { - // The `todoEditView` has all the logic for displaying the todo and editing. - let todoEditView: TodoEditView - - required init(screen: TodoEditScreen, environment: ViewEnvironment) { - self.todoEditView = TodoEditView(frame: .zero) - - super.init(screen: screen, environment: environment) - update(with: screen) - } - - override func viewDidLoad() { - super.viewDidLoad() - - view.addSubview(todoEditView) - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - - todoEditView.frame = view.bounds.inset(by: view.safeAreaInsets) - } - - override func screenDidChange(from previousScreen: TodoEditScreen, previousEnvironment: ViewEnvironment) { - update(with: screen) - } - - private func update(with screen: TodoEditScreen) { - // Update the view with the data from the screen. - todoEditView.title = screen.title - todoEditView.note = screen.note - todoEditView.onTitleChanged = screen.onTitleChanged - todoEditView.onNoteChanged = screen.onNoteChanged - } -} diff --git a/swift/Samples/Tutorial/Frameworks/Tutorial5Complete/Sources/Todo/Edit/TodoEditWorkflow.swift b/swift/Samples/Tutorial/Frameworks/Tutorial5Complete/Sources/Todo/Edit/TodoEditWorkflow.swift deleted file mode 100644 index 2eb97716e..000000000 --- a/swift/Samples/Tutorial/Frameworks/Tutorial5Complete/Sources/Todo/Edit/TodoEditWorkflow.swift +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 BackStackContainer -import ReactiveSwift -import Workflow -import WorkflowUI - -// MARK: Input and Output - -struct TodoEditWorkflow: Workflow { - // The "Todo" passed from our parent. - var initialTodo: TodoModel - - enum Output { - case discard - case save(TodoModel) - } -} - -// MARK: State and Initialization - -extension TodoEditWorkflow { - struct State { - // The workflow's copy of the Todo item. Changes are local to this workflow. - var todo: TodoModel - } - - func makeInitialState() -> TodoEditWorkflow.State { - return State(todo: initialTodo) - } - - func workflowDidChange(from previousWorkflow: TodoEditWorkflow, state: inout State) { - // The `Todo` from our parent changed. Update our internal copy so we are starting from the same item. - // The "correct" behavior depends on the business logic - would we only want to update if the - // users hasn't changed the todo from the initial one? Or is it ok to delete whatever edits - // were in progress if the state from the parent changes? - if previousWorkflow.initialTodo != initialTodo { - state.todo = initialTodo - } - } -} - -// MARK: Actions - -extension TodoEditWorkflow { - enum Action: WorkflowAction { - typealias WorkflowType = TodoEditWorkflow - - case titleChanged(String) - case noteChanged(String) - case discardChanges - case saveChanges - - func apply(toState state: inout TodoEditWorkflow.State) -> TodoEditWorkflow.Output? { - switch self { - case let .titleChanged(title): - state.todo.title = title - - case let .noteChanged(note): - state.todo.note = note - - case .discardChanges: - // Return the .discard output when the discard action is received. - return .discard - - case .saveChanges: - // Return the .save output with the current todo state when the save action is received. - return .save(state.todo) - } - - return nil - } - } -} - -// MARK: Workers - -extension TodoEditWorkflow { - struct TodoEditWorker: Worker { - enum Output {} - - func run() -> SignalProducer { - fatalError() - } - - func isEquivalent(to otherWorker: TodoEditWorker) -> Bool { - return true - } - } -} - -// MARK: Rendering - -extension TodoEditWorkflow { - typealias Rendering = BackStackScreen.Item - - func render(state: TodoEditWorkflow.State, context: RenderContext) -> Rendering { - // The sink is used to send actions back to this workflow. - let sink = context.makeSink(of: Action.self) - - let todoEditScreen = TodoEditScreen( - title: state.todo.title, - note: state.todo.note, - onTitleChanged: { title in - sink.send(.titleChanged(title)) - }, - onNoteChanged: { note in - sink.send(.noteChanged(note)) - } - ) - - let backStackItem = BackStackScreen.Item( - key: "edit", - screen: todoEditScreen.asAnyScreen(), - barContent: .init( - title: "Edit", - leftItem: .button(.back(handler: { - sink.send(.discardChanges) - })), - rightItem: .button(.init( - content: .text("Save"), - handler: { - sink.send(.saveChanges) - } - )) - ) - ) - return backStackItem - } -} diff --git a/swift/Samples/Tutorial/Frameworks/Tutorial5Complete/Sources/Todo/List/TodoListScreen.swift b/swift/Samples/Tutorial/Frameworks/Tutorial5Complete/Sources/Todo/List/TodoListScreen.swift deleted file mode 100644 index 7108db7dd..000000000 --- a/swift/Samples/Tutorial/Frameworks/Tutorial5Complete/Sources/Todo/List/TodoListScreen.swift +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 TutorialViews -import Workflow -import WorkflowUI - -struct TodoListScreen: Screen { - // The titles of the todo items - var todoTitles: [String] - - // Callback when a todo is selected - var onTodoSelected: (Int) -> Void - - func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { - return TodoListViewController.description(for: self, environment: environment) - } -} - -final class TodoListViewController: ScreenViewController { - let todoListView: TodoListView - - required init(screen: TodoListScreen, environment: ViewEnvironment) { - self.todoListView = TodoListView(frame: .zero) - super.init(screen: screen, environment: environment) - update(with: screen) - } - - override func viewDidLoad() { - super.viewDidLoad() - - view.addSubview(todoListView) - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - - todoListView.frame = view.bounds.inset(by: view.safeAreaInsets) - } - - override func screenDidChange(from previousScreen: TodoListScreen, previousEnvironment: ViewEnvironment) { - update(with: screen) - } - - private func update(with screen: TodoListScreen) { - // Update the todoList on the view with what the screen provided: - todoListView.todoList = screen.todoTitles - todoListView.onTodoSelected = screen.onTodoSelected - } -} diff --git a/swift/Samples/Tutorial/Frameworks/Tutorial5Complete/Sources/Todo/List/TodoListWorkflow.swift b/swift/Samples/Tutorial/Frameworks/Tutorial5Complete/Sources/Todo/List/TodoListWorkflow.swift deleted file mode 100644 index 3331c9bc9..000000000 --- a/swift/Samples/Tutorial/Frameworks/Tutorial5Complete/Sources/Todo/List/TodoListWorkflow.swift +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 BackStackContainer -import ReactiveSwift -import Workflow -import WorkflowUI - -// MARK: Input and Output - -struct TodoListWorkflow: Workflow { - // The name is an input. - var name: String - // Use the list of todo items passed from our parent. - var todos: [TodoModel] - - enum Output { - case back - case selectTodo(index: Int) - case newTodo - } -} - -// MARK: State and Initialization - -extension TodoListWorkflow { - struct State {} - - func makeInitialState() -> TodoListWorkflow.State { - return State() - } - - func workflowDidChange(from previousWorkflow: TodoListWorkflow, state: inout State) {} -} - -// MARK: Actions - -extension TodoListWorkflow { - enum Action: WorkflowAction { - typealias WorkflowType = TodoListWorkflow - - case onBack - case selectTodo(index: Int) - case new - - func apply(toState state: inout TodoListWorkflow.State) -> TodoListWorkflow.Output? { - switch self { - case .onBack: - // When a `.onBack` action is received, emit a `.back` output - return .back - - case let .selectTodo(index: index): - // Tell our parent that a todo item was selected. - return .selectTodo(index: index) - - case .new: - // Tell our parent a new todo item should be created. - return .newTodo - } - } - } -} - -// MARK: Workers - -extension TodoListWorkflow { - struct TodoListWorker: Worker { - enum Output {} - - func run() -> SignalProducer { - fatalError() - } - - func isEquivalent(to otherWorker: TodoListWorker) -> Bool { - return true - } - } -} - -// MARK: Rendering - -extension TodoListWorkflow { - typealias Rendering = BackStackScreen.Item - - func render(state: TodoListWorkflow.State, context: RenderContext) -> Rendering { - // Define a sink to be able to send the .onBack action. - let sink = context.makeSink(of: Action.self) - - let titles = todos.map { (todoModel) -> String in - todoModel.title - } - let todoListScreen = TodoListScreen( - todoTitles: titles, - onTodoSelected: { index in - // Send the `selectTodo` action when a todo is selected in the UI. - sink.send(.selectTodo(index: index)) - } - ) - - let todoListItem = BackStackScreen.Item( - key: "list", - screen: todoListScreen.asAnyScreen(), - barContent: .init( - title: "Welcome \(name)", - leftItem: .button(.back(handler: { - // When the left button is tapped, send the .onBack action. - sink.send(.onBack) - })), - rightItem: .button(.init( - content: .text("New Todo"), - handler: { - sink.send(.new) - } - )) - ) - ) - - return todoListItem - } -} diff --git a/swift/Samples/Tutorial/Frameworks/Tutorial5Complete/Sources/Todo/Model/TodoModel.swift b/swift/Samples/Tutorial/Frameworks/Tutorial5Complete/Sources/Todo/Model/TodoModel.swift deleted file mode 100644 index 4aa909e03..000000000 --- a/swift/Samples/Tutorial/Frameworks/Tutorial5Complete/Sources/Todo/Model/TodoModel.swift +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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. - */ - -struct TodoModel: Equatable { - var title: String - var note: String -} diff --git a/swift/Samples/Tutorial/Frameworks/Tutorial5Complete/Sources/Todo/TodoWorkflow.swift b/swift/Samples/Tutorial/Frameworks/Tutorial5Complete/Sources/Todo/TodoWorkflow.swift deleted file mode 100644 index 2e2208901..000000000 --- a/swift/Samples/Tutorial/Frameworks/Tutorial5Complete/Sources/Todo/TodoWorkflow.swift +++ /dev/null @@ -1,181 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 BackStackContainer -import ReactiveSwift -import Workflow -import WorkflowUI - -// MARK: Input and Output - -struct TodoWorkflow: Workflow { - var name: String - - enum Output { - case back - } -} - -// MARK: State and Initialization - -extension TodoWorkflow { - struct State: Equatable { - var todos: [TodoModel] - var step: Step - enum Step: Equatable { - // Showing the list of todo items. - case list - // Editing a single item. The state holds the index so it can be updated when a save action is received. - case edit(index: Int) - } - } - - func makeInitialState() -> TodoWorkflow.State { - return State( - todos: [ - TodoModel( - title: "Take the cat for a walk", - note: "Cats really need their outside sunshine time. Don't forget to walk Charlie. Hamilton is less excited about the prospect." - ), - ], - step: .list - ) - } - - func workflowDidChange(from previousWorkflow: TodoWorkflow, state: inout State) {} -} - -// MARK: Actions - -extension TodoWorkflow { - enum ListAction: WorkflowAction { - typealias WorkflowType = TodoWorkflow - - case back - case editTodo(index: Int) - case newTodo - - func apply(toState state: inout TodoWorkflow.State) -> TodoWorkflow.Output? { - switch self { - case .back: - return .back - - case let .editTodo(index: index): - state.step = .edit(index: index) - - case .newTodo: - // Append a new todo model to the end of the list. - state.todos.append(TodoModel( - title: "New Todo", - note: "" - )) - } - - return nil - } - } - - enum EditAction: WorkflowAction { - typealias WorkflowType = TodoWorkflow - - case discardChanges - case saveChanges(index: Int, todo: TodoModel) - - func apply(toState state: inout TodoWorkflow.State) -> TodoWorkflow.Output? { - guard case .edit = state.step else { - fatalError("Received edit action when state was not `.edit`.") - } - - switch self { - case .discardChanges: - state.step = .list - - case let .saveChanges(index: index, todo: updatedTodo): - state.todos[index] = updatedTodo - } - // Return to the list view for either a discard or save action. - state.step = .list - - return nil - } - } -} - -// MARK: Workers - -extension TodoWorkflow { - struct TodoWorker: Worker { - enum Output {} - - func run() -> SignalProducer { - fatalError() - } - - func isEquivalent(to otherWorker: TodoWorker) -> Bool { - return true - } - } -} - -// MARK: Rendering - -extension TodoWorkflow { - typealias Rendering = [BackStackScreen.Item] - - func render(state: TodoWorkflow.State, context: RenderContext) -> Rendering { - let todoListItem = TodoListWorkflow( - name: name, - todos: state.todos - ) - .mapOutput { output -> ListAction in - switch output { - case .back: - return .back - - case let .selectTodo(index: index): - return .editTodo(index: index) - - case .newTodo: - return .newTodo - } - } - .rendered(with: context) - - switch state.step { - case .list: - // Return only the list item. - return [todoListItem] - - case let .edit(index: index): - - let todoEditItem = TodoEditWorkflow( - initialTodo: state.todos[index]) - .mapOutput { output -> EditAction in - switch output { - case .discard: - return .discardChanges - - case let .save(updatedTodo): - return .saveChanges(index: index, todo: updatedTodo) - } - } - .rendered(with: context) - - // Return both the list item and edit. - return [todoListItem, todoEditItem] - } - } -} diff --git a/swift/Samples/Tutorial/Frameworks/Tutorial5Complete/Sources/TutorialContainerViewController.swift b/swift/Samples/Tutorial/Frameworks/Tutorial5Complete/Sources/TutorialContainerViewController.swift deleted file mode 100644 index 2a8770166..000000000 --- a/swift/Samples/Tutorial/Frameworks/Tutorial5Complete/Sources/TutorialContainerViewController.swift +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 BackStackContainer -import UIKit -import Workflow -import WorkflowUI - -public final class TutorialContainerViewController: UIViewController { - let containerViewController: UIViewController - - public init() { - // Create a `ContainerViewController` with the `RootWorkflow` as the root workflow - self.containerViewController = ContainerViewController(workflow: RootWorkflow()) - - super.init(nibName: nil, bundle: nil) - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override public func viewDidLoad() { - super.viewDidLoad() - - view.backgroundColor = .white - - addChild(containerViewController) - view.addSubview(containerViewController.view) - containerViewController.didMove(toParent: self) - } - - override public func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - - containerViewController.view.frame = view.bounds - } -} diff --git a/swift/Samples/Tutorial/Frameworks/Tutorial5Complete/Sources/Welcome/WelcomeScreen.swift b/swift/Samples/Tutorial/Frameworks/Tutorial5Complete/Sources/Welcome/WelcomeScreen.swift deleted file mode 100644 index 474033f3a..000000000 --- a/swift/Samples/Tutorial/Frameworks/Tutorial5Complete/Sources/Welcome/WelcomeScreen.swift +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 TutorialViews -import Workflow -import WorkflowUI - -struct WelcomeScreen: Screen { - /// The current name that has been entered. - var name: String - /// Callback when the name changes in the UI. - var onNameChanged: (String) -> Void - /// Callback when the login button is tapped. - var onLoginTapped: () -> Void - - func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { - return WelcomeViewController.description(for: self, environment: environment) - } -} - -final class WelcomeViewController: ScreenViewController { - var welcomeView: WelcomeView - - required init(screen: WelcomeScreen, environment: ViewEnvironment) { - self.welcomeView = WelcomeView(frame: .zero) - super.init(screen: screen, environment: environment) - update(with: screen) - } - - override func viewDidLoad() { - super.viewDidLoad() - - view.addSubview(welcomeView) - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - - welcomeView.frame = view.bounds.inset(by: view.safeAreaInsets) - } - - override func screenDidChange(from previousScreen: WelcomeScreen, previousEnvironment: ViewEnvironment) { - update(with: screen) - } - - private func update(with screen: WelcomeScreen) { - /// Update UI - welcomeView.name = screen.name - welcomeView.onNameChanged = screen.onNameChanged - welcomeView.onLoginTapped = screen.onLoginTapped - } -} diff --git a/swift/Samples/Tutorial/Frameworks/Tutorial5Complete/Sources/Welcome/WelcomeWorkflow.swift b/swift/Samples/Tutorial/Frameworks/Tutorial5Complete/Sources/Welcome/WelcomeWorkflow.swift deleted file mode 100644 index 395361779..000000000 --- a/swift/Samples/Tutorial/Frameworks/Tutorial5Complete/Sources/Welcome/WelcomeWorkflow.swift +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 ReactiveSwift -import Workflow -import WorkflowUI - -// MARK: Input and Output - -struct WelcomeWorkflow: Workflow { - enum Output: Equatable { - case didLogin(name: String) - } -} - -// MARK: State and Initialization - -extension WelcomeWorkflow { - struct State: Equatable { - var name: String - } - - func makeInitialState() -> WelcomeWorkflow.State { - return State(name: "") - } - - func workflowDidChange(from previousWorkflow: WelcomeWorkflow, state: inout State) {} -} - -// MARK: Actions - -extension WelcomeWorkflow { - enum Action: WorkflowAction { - typealias WorkflowType = WelcomeWorkflow - - case nameChanged(name: String) - case didLogin - - func apply(toState state: inout WelcomeWorkflow.State) -> WelcomeWorkflow.Output? { - switch self { - case let .nameChanged(name: name): - // Update our state with the updated name. - state.name = name - // Return `nil` for the output, we want to handle this action only at the level of this workflow. - return nil - - case .didLogin: - if state.name.count != 0 { - // Return an output of `didLogin` with the name if it's not empty. - return .didLogin(name: state.name) - } else { - // Don't log in if the name isn't filled in. - return nil - } - } - } - } -} - -// MARK: Workers - -extension WelcomeWorkflow { - struct WelcomeWorker: Worker { - enum Output {} - - func run() -> SignalProducer { - fatalError() - } - - func isEquivalent(to otherWorker: WelcomeWorker) -> Bool { - return true - } - } -} - -// MARK: Rendering - -extension WelcomeWorkflow { - typealias Rendering = WelcomeScreen - - func render(state: WelcomeWorkflow.State, context: RenderContext) -> Rendering { - // Create a "sink" of type `Action`. A sink is what we use to send actions to the workflow. - let sink = context.makeSink(of: Action.self) - - return WelcomeScreen( - name: state.name, - onNameChanged: { name in - sink.send(.nameChanged(name: name)) - }, - onLoginTapped: { - // Whenever the login button is tapped, emit the `.didLogin` action. - sink.send(.didLogin) - } - ) - } -} diff --git a/swift/Samples/Tutorial/Frameworks/Tutorial5Complete/Tests/RootWorkflowTests.swift b/swift/Samples/Tutorial/Frameworks/Tutorial5Complete/Tests/RootWorkflowTests.swift deleted file mode 100644 index c49c01b11..000000000 --- a/swift/Samples/Tutorial/Frameworks/Tutorial5Complete/Tests/RootWorkflowTests.swift +++ /dev/null @@ -1,238 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 Workflow -import WorkflowTesting -import XCTest -// Import `BackStackContainer` as testable so that the items in the `BackStackScreen` can be inspected. -@testable import BackStackContainer -@testable import Tutorial5 -// Import `WorkflowUI` as testable so that the wrappedScreen in `AnyScreen` can be accessed. -@testable import WorkflowUI - -class RootWorkflowTests: XCTestCase { - func testWelcomeRendering() { - RootWorkflow() - // Start in the `.welcome` state - .renderTester(initialState: RootWorkflow.State.welcome) - .render( - // Expect the state to stay as `.welcome`. - expectedState: ExpectedState(state: RootWorkflow.State.welcome), - // No output is expected from the root workflow. - expectedOutput: nil, - // There are no workers that should be run. - expectedWorkers: [], - // The `WelcomeWorkflow` is expected to be started in this render. - expectedWorkflows: [ - ExpectedWorkflow( - type: WelcomeWorkflow.self, - // Simulate this as the `WelcomeScreen` returned by the `WelcomeWorkflow`. The callback can be stubbed out, as they won't be used. - rendering: WelcomeScreen( - name: "MyName", - onNameChanged: { _ in }, - onLoginTapped: {} - ) - ), - ], - // Now, validate that there is a single item in the BackStackScreen, which is our welcome screen. - assertions: { rendering in - XCTAssertEqual(1, rendering.items.count) - guard let welcomeScreen = rendering.items[0].screen.wrappedScreen as? WelcomeScreen else { - XCTFail("Expected first screen to be a `WelcomeScreen`") - return - } - XCTAssertEqual("MyName", welcomeScreen.name) - } - ) - } - - func testLogin() { - RootWorkflow() - // Start in the `.welcome` state - .renderTester(initialState: RootWorkflow.State.welcome) - .render( - // Expect the state to transition to `.todo` - expectedState: ExpectedState(state: RootWorkflow.State.todo(name: "MyName")), - // No output is expected from the root workflow. - expectedOutput: nil, - // There are no workers that should be run. - expectedWorkers: [], - // The `WelcomeWorkflow` is expected to be started in this render. - expectedWorkflows: [ - ExpectedWorkflow( - type: WelcomeWorkflow.self, - // Simulate this as the `WelcomeScreen` returned by the `WelcomeWorkflow`. The callback can be stubbed out, as they won't be used. - rendering: WelcomeScreen( - name: "MyName", - onNameChanged: { _ in }, - onLoginTapped: {} - ), - // Simulate the `WelcomeWorkflow` sending an output of `.didLogin` as if the login button was tapped. - output: .didLogin(name: "MyName") - ), - ], - // Now, validate that there is a single item in the BackStackScreen, which is our welcome screen (prior to the output). - assertions: { rendering in - XCTAssertEqual(1, rendering.items.count) - guard let welcomeScreen = rendering.items[0].screen.wrappedScreen as? WelcomeScreen else { - XCTFail("Expected first screen to be a `WelcomeScreen`") - return - } - XCTAssertEqual("MyName", welcomeScreen.name) - } - ) - .assert(state: { state in - XCTAssertEqual(.todo(name: "MyName"), state) - }) - } - - func testAppFlow() { - let workflowHost = WorkflowHost(workflow: RootWorkflow()) - - // First rendering is just the welcome screen. Update the name. - do { - let backStack = workflowHost.rendering.value - XCTAssertEqual(1, backStack.items.count) - - guard let welcomeScreen = backStack.items[0].screen.wrappedScreen as? WelcomeScreen else { - XCTFail("Expected initial screen of `WelcomeScreen`") - return - } - - welcomeScreen.onNameChanged("MyName") - } - - // Log in and go to the welcome list - do { - let backStack = workflowHost.rendering.value - XCTAssertEqual(1, backStack.items.count) - - guard let welcomeScreen = backStack.items[0].screen.wrappedScreen as? WelcomeScreen else { - XCTFail("Expected initial screen of `WelcomeScreen`") - return - } - - welcomeScreen.onLoginTapped() - } - - // Expect the todo list. Edit the first todo. - do { - let backStack = workflowHost.rendering.value - XCTAssertEqual(2, backStack.items.count) - - guard let _ = backStack.items[0].screen.wrappedScreen as? WelcomeScreen else { - XCTFail("Expected first screen of `WelcomeScreen`") - return - } - - guard let todoScreen = backStack.items[1].screen.wrappedScreen as? TodoListScreen else { - XCTFail("Expected second screen of `TodoListScreen`") - return - } - XCTAssertEqual(1, todoScreen.todoTitles.count) - // Select the first todo: - todoScreen.onTodoSelected(0) - } - - // Selected a todo to edit. Expect the todo edit screen. - do { - let backStack = workflowHost.rendering.value - XCTAssertEqual(3, backStack.items.count) - - guard let _ = backStack.items[0].screen.wrappedScreen as? WelcomeScreen else { - XCTFail("Expected first screen of `WelcomeScreen`") - return - } - - guard let _ = backStack.items[1].screen.wrappedScreen as? TodoListScreen else { - XCTFail("Expected second screen of `TodoListScreen`") - return - } - - guard let editScreen = backStack.items[2].screen.wrappedScreen as? TodoEditScreen else { - XCTFail("Expected second screen of `TodoEditScreen`") - return - } - - // Update the title: - editScreen.onTitleChanged("New Title") - } - - // Save the selected todo. - do { - let backStack = workflowHost.rendering.value - XCTAssertEqual(3, backStack.items.count) - - guard let _ = backStack.items[0].screen.wrappedScreen as? WelcomeScreen else { - XCTFail("Expected first screen of `WelcomeScreen`") - return - } - - guard let _ = backStack.items[1].screen.wrappedScreen as? TodoListScreen else { - XCTFail("Expected second screen of `TodoListScreen`") - return - } - - guard let _ = backStack.items[2].screen.wrappedScreen as? TodoEditScreen else { - XCTFail("Expected second screen of `TodoEditScreen`") - return - } - - // Save the changes by tapping the right bar button. - // This also validates that the navigation bar was described as expected. - switch backStack.items[2].barVisibility { - case .hidden: - XCTFail("Expected a visible navigation bar") - - case let .visible(barContent): - switch barContent.rightItem { - case .none: - XCTFail("Expected a right bar button") - - case let .button(button): - - switch button.content { - case let .text(text): - XCTAssertEqual("Save", text) - - case .icon: - XCTFail("Expected the right bar button to have a title of `Save`") - } - // Tap the right bar button to save. - button.handler() - } - } - } - - // Expect the todo list. Validate the title was updated. - do { - let backStack = workflowHost.rendering.value - XCTAssertEqual(2, backStack.items.count) - - guard let _ = backStack.items[0].screen.wrappedScreen as? WelcomeScreen else { - XCTFail("Expected first screen of `WelcomeScreen`") - return - } - - guard let todoScreen = backStack.items[1].screen.wrappedScreen as? TodoListScreen else { - XCTFail("Expected second screen of `TodoListScreen`") - return - } - XCTAssertEqual(1, todoScreen.todoTitles.count) - XCTAssertEqual("New Title", todoScreen.todoTitles[0]) - } - } -} diff --git a/swift/Samples/Tutorial/Frameworks/Tutorial5Complete/Tests/TodoEditWorkflowTests.swift b/swift/Samples/Tutorial/Frameworks/Tutorial5Complete/Tests/TodoEditWorkflowTests.swift deleted file mode 100644 index 9de7646c9..000000000 --- a/swift/Samples/Tutorial/Frameworks/Tutorial5Complete/Tests/TodoEditWorkflowTests.swift +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 WorkflowTesting -import XCTest -@testable import Tutorial5 - -class TodoEditWorkflowTests: XCTestCase { - func testAction() { - TodoEditWorkflow - .Action - // Start with a todo of "Title" "Note" - .tester( - withState: TodoEditWorkflow.State( - todo: TodoModel(title: "Title", note: "Note"))) - .assertState { state in - XCTAssertEqual("Title", state.todo.title) - XCTAssertEqual("Note", state.todo.note) - } - // Update the title to "Updated Title" - .send(action: .titleChanged("Updated Title")) { output in - XCTAssertNil(output) - } - // Validate that only the title changed. - .assertState { state in - XCTAssertEqual("Updated Title", state.todo.title) - XCTAssertEqual("Note", state.todo.note) - } - // Update the note. - .send(action: .noteChanged("Updated Note")) { output in - XCTAssertNil(output) - } - // Validate that the note updated. - .assertState { state in - XCTAssertEqual("Updated Title", state.todo.title) - XCTAssertEqual("Updated Note", state.todo.note) - } - // Send a `.discardChanges` action, which will emit a `.discard` output. - .send(action: .discardChanges) { output in - switch output { - case .discard?: - break // Expected - default: - XCTFail("Expected an output of `.discard`") - } - } - // Send a `.saveChanges` action, which will emit a `.save` output with the updated todo model. - .send(action: .saveChanges) { output in - switch output { - case let .save(todo)?: - XCTAssertEqual("Updated Title", todo.title) - XCTAssertEqual("Updated Note", todo.note) - default: - XCTFail("Expected an output of `.save`") - } - } - } - - func testChangedPropertyUpdatesLocalState() { - let initialWorkflow = TodoEditWorkflow(initialTodo: TodoModel(title: "Title", note: "Note")) - var state = initialWorkflow.makeInitialState() - // The initial state is a copy of the provided todo: - XCTAssertEqual("Title", state.todo.title) - XCTAssertEqual("Note", state.todo.note) - - // Mutate the internal state, simulating the change from actions: - state.todo.title = "Updated Title" - - // Update the workflow properties with the same value. The state should not be updated: - initialWorkflow.workflowDidChange(from: initialWorkflow, state: &state) - XCTAssertEqual("Updated Title", state.todo.title) - XCTAssertEqual("Note", state.todo.note) - - // The parent provided different properties. The internal state should be updated with the newly provided properties. - let updatedWorkflow = TodoEditWorkflow(initialTodo: TodoModel(title: "New Title", note: "New Note")) - updatedWorkflow.workflowDidChange(from: initialWorkflow, state: &state) - - XCTAssertEqual("New Title", state.todo.title) - XCTAssertEqual("New Note", state.todo.note) - } -} diff --git a/swift/Samples/Tutorial/Frameworks/Tutorial5Complete/Tests/TodoListWorkflowTests.swift b/swift/Samples/Tutorial/Frameworks/Tutorial5Complete/Tests/TodoListWorkflowTests.swift deleted file mode 100644 index 8a4ef2bca..000000000 --- a/swift/Samples/Tutorial/Frameworks/Tutorial5Complete/Tests/TodoListWorkflowTests.swift +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 WorkflowTesting -import XCTest -@testable import Tutorial5 - -class TodoListWorkflowTests: XCTestCase { - func testActions() { - TodoListWorkflow - .Action - .tester(withState: TodoListWorkflow.State()) - .send(action: .onBack) { output in - // The `.onBack` action should emit an output of `.back`. - switch output { - case .back?: - break // Expected - default: - XCTFail("Expected an output of `.back`") - } - } - .send(action: .selectTodo(index: 7)) { output in - // The `.selectTodo` action should emit a `.selectTodo` output. - switch output { - case let .selectTodo(index)?: - XCTAssertEqual(7, index) - default: - XCTFail("Expected an output of `.selectTodo`") - } - } - .send(action: .new) { output in - // The`.new` action should emit a `.newTodo` output. - switch output { - case .newTodo?: - break // Expected - default: - XCTFail("Expected an output of `.newTodo`") - } - } - } -} diff --git a/swift/Samples/Tutorial/Frameworks/Tutorial5Complete/Tests/TodoWorkflowTests.swift b/swift/Samples/Tutorial/Frameworks/Tutorial5Complete/Tests/TodoWorkflowTests.swift deleted file mode 100644 index 021d201cf..000000000 --- a/swift/Samples/Tutorial/Frameworks/Tutorial5Complete/Tests/TodoWorkflowTests.swift +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 BackStackContainer -import WorkflowTesting -import XCTest -import Workflow -@testable import Tutorial5 - -class TodoWorkflowTests: XCTestCase { - func testSelectingTodo() { - let todos: [TodoModel] = [TodoModel(title: "Title", note: "Note")] - - TodoWorkflow(name: "MyName") - // Start from the list step to validate selecting a todo: - .renderTester(initialState: TodoWorkflow.State( - todos: todos, - step: .list - )) - .render( - // Only specify the expected workflows for this render: - expectedWorkflows: [ - // We only expect the TodoListWorkflow - ExpectedWorkflow( - type: TodoListWorkflow.self, - rendering: BackStackScreen.Item( - screen: TodoListScreen( - todoTitles: ["Title"], - onTodoSelected: { _ in } - ).asAnyScreen()), - // Simulate selecting the first todo: - output: TodoListWorkflow.Output.selectTodo(index: 0) - ), - ], - assertions: { items in - // Just validate that there is one item in the backstack. - // Additional validation could be done on the screens returned if so desired. - XCTAssertEqual(1, items.count) - } - ) - // Validate that the state was updated after the last render pass with the output from the TodoEditWorkflow. - .assert { state in - XCTAssertEqual( - TodoWorkflow.State( - todos: [TodoModel(title: "Title", note: "Note")], - step: .edit(index: 0) - ), - state - ) - } - } - - func testSavingTodo() { - let todos: [TodoModel] = [TodoModel(title: "Title", note: "Note")] - - TodoWorkflow(name: "MyName") - // Start from the edit step so we can simulate saving: - .renderTester(initialState: TodoWorkflow.State( - todos: todos, - step: .edit(index: 0) - )) - .render( - // Only specify the expected workflows for this render: - expectedWorkflows: [ - // We always expect the TodoListWorkflow - ExpectedWorkflow( - type: TodoListWorkflow.self, - rendering: BackStackScreen.Item( - screen: TodoListScreen( - todoTitles: ["Title"], - onTodoSelected: { _ in } - ).asAnyScreen()) - ), - // Expect the TodoEditWorkflow. Additionally, simulate it emitting an output of ".save" to update the state. - ExpectedWorkflow( - type: TodoEditWorkflow.self, - rendering: BackStackScreen.Item(screen: TodoEditScreen( - title: "Title", - note: "Note", - onTitleChanged: { _ in }, - onNoteChanged: { _ in } - ).asAnyScreen()), - output: TodoEditWorkflow.Output.save(TodoModel( - title: "Updated Title", - note: "Updated Note" - )) - ), - ], - assertions: { items in - // Just validate that there are two items in the backstack. - // Additional validation could be done on the screens returned if so desired. - XCTAssertEqual(2, items.count) - } - ) - // Validate that the state was updated after the last render pass with the output from the TodoEditWorkflow. - .assert { state in - XCTAssertEqual( - TodoWorkflow.State( - todos: [TodoModel(title: "Updated Title", note: "Updated Note")], - step: .list - ), - state - ) - } - } -} diff --git a/swift/Samples/Tutorial/Frameworks/Tutorial5Complete/Tests/TutorialTests.swift b/swift/Samples/Tutorial/Frameworks/Tutorial5Complete/Tests/TutorialTests.swift deleted file mode 100644 index f5b45e2a2..000000000 --- a/swift/Samples/Tutorial/Frameworks/Tutorial5Complete/Tests/TutorialTests.swift +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 XCTest - -final class TutorialTests: XCTestCase { - func testPlaceholder() { - XCTAssertEqual(1, 1, "Placeholder test") - } -} diff --git a/swift/Samples/Tutorial/Frameworks/Tutorial5Complete/Tests/WelcomeWorkflowTests.swift b/swift/Samples/Tutorial/Frameworks/Tutorial5Complete/Tests/WelcomeWorkflowTests.swift deleted file mode 100644 index e7560960a..000000000 --- a/swift/Samples/Tutorial/Frameworks/Tutorial5Complete/Tests/WelcomeWorkflowTests.swift +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 WorkflowTesting -import XCTest -@testable import Tutorial5 - -class WelcomeWorkflowTests: XCTestCase { - func testNameUpdates() { - WelcomeWorkflow.Action - .tester(withState: WelcomeWorkflow.State(name: "")) - .assertState { state in - // The initial state provided was an empty name. - XCTAssertEqual("", state.name) - } - .send(action: .nameChanged(name: "myName")) { output in - // No output is expected when the name changes. - XCTAssertNil(output) - } - .assertState { state in - // The `name` has been updated from the action. - XCTAssertEqual("myName", state.name) - } - } - - func testLogin() { - WelcomeWorkflow.Action - .tester(withState: WelcomeWorkflow.State(name: "")) - .send(action: .didLogin) { output in - // Since the name is empty, `.didLogin` will not emit an output. - XCTAssertNil(output) - } - .assertState { state in - // The name is empty, as was specified in the initial state. - XCTAssertEqual("", state.name) - } - .send(action: .nameChanged(name: "MyName")) { output in - // Update the name. - XCTAssertNil(output) - } - .assertState { state in - // Validate the name was updated. - XCTAssertEqual("MyName", state.name) - } - .send(action: .didLogin) { output in - // Now a `.didLogin` output should be emitted when the `.didLogin` action was received. - switch output { - case let .didLogin(name)?: - XCTAssertEqual("MyName", name) - case nil: - XCTFail("Did not receive an output for .didLogin") - } - } - } - - func testRendering() { - WelcomeWorkflow() - // Use the initial state provided by the welcome workflow - .renderTester() - .render(assertions: { screen in - XCTAssertEqual("", screen.name) - // Simulate tapping the login button. No output will be emitted, as the name is empty: - screen.onLoginTapped() - }) - // Next, simulate the name updating, expecting the state to be changed to reflect the updated name: - .render( - expectedState: ExpectedState(state: WelcomeWorkflow.State(name: "myName")), - assertions: { screen in - screen.onNameChanged("myName") - } - ) - // Finally, validate that `.didLogin` is sent when login is tapped with a non-empty name: - .render( - expectedOutput: ExpectedOutput(output: .didLogin(name: "myName")), - assertions: { screen in - screen.onLoginTapped() - } - ) - } -} diff --git a/swift/Samples/Tutorial/Frameworks/Tutorial5Complete/Tutorial5.podspec b/swift/Samples/Tutorial/Frameworks/Tutorial5Complete/Tutorial5.podspec deleted file mode 100644 index 046542300..000000000 --- a/swift/Samples/Tutorial/Frameworks/Tutorial5Complete/Tutorial5.podspec +++ /dev/null @@ -1,30 +0,0 @@ -Pod::Spec.new do |s| - s.name = 'Tutorial5' - s.version = '1.0.0.LOCAL' - s.summary = 'See the README.' - s.homepage = 'https://github.com/square/workflow' - s.license = 'Apache License, Version 2.0' - s.author = 'Square' - s.source = { git: 'Not Published', tag: "podify/#{s.version}" } - - # 1.7 is needed for `swift_versions` support - s.cocoapods_version = '>= 1.7.0' - - s.swift_versions = ['5.0'] - s.ios.deployment_target = '11.0' - - s.source_files = 'Sources/**/*.swift' - s.resource_bundle = { 'TutorialResources' => ['Resources/**/*'] } - - s.dependency 'TutorialViews' - s.dependency 'Workflow' - s.dependency 'WorkflowUI' - s.dependency 'BackStackContainer' - - s.test_spec 'Tests' do |test_spec| - test_spec.source_files = 'Tests/**/*.swift' - - test_spec.framework = 'XCTest' - test_spec.dependency 'WorkflowTesting' - end -end diff --git a/swift/Samples/Tutorial/Frameworks/TutorialBase/Sources/Todo/Edit/TodoEditSampleViewController.swift b/swift/Samples/Tutorial/Frameworks/TutorialBase/Sources/Todo/Edit/TodoEditSampleViewController.swift deleted file mode 100644 index 17e596b7f..000000000 --- a/swift/Samples/Tutorial/Frameworks/TutorialBase/Sources/Todo/Edit/TodoEditSampleViewController.swift +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 TutorialViews - -final class TodoEditSampleViewController: UIViewController { - let todoEditView: TodoEditView - - init() { - self.todoEditView = TodoEditView(frame: .zero) - - super.init(nibName: nil, bundle: nil) - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: UIViewController - - override func viewDidLoad() { - super.viewDidLoad() - - view.addSubview(todoEditView) - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - - todoEditView.frame = view.bounds.inset(by: view.safeAreaInsets) - } -} diff --git a/swift/Samples/Tutorial/Frameworks/TutorialBase/Sources/Todo/List/TodoListSampleViewController.swift b/swift/Samples/Tutorial/Frameworks/TutorialBase/Sources/Todo/List/TodoListSampleViewController.swift deleted file mode 100644 index c0b40e0c1..000000000 --- a/swift/Samples/Tutorial/Frameworks/TutorialBase/Sources/Todo/List/TodoListSampleViewController.swift +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 TutorialViews - -final class TodoListSampleViewController: UIViewController { - let todoListView: TodoListView - - init() { - self.todoListView = TodoListView(frame: .zero) - - super.init(nibName: nil, bundle: nil) - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: UIViewController - - override func viewDidLoad() { - super.viewDidLoad() - - view.addSubview(todoListView) - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - - todoListView.frame = view.bounds.inset(by: view.safeAreaInsets) - } -} diff --git a/swift/Samples/Tutorial/Frameworks/TutorialBase/Sources/Todo/Model/TodoModel.swift b/swift/Samples/Tutorial/Frameworks/TutorialBase/Sources/Todo/Model/TodoModel.swift deleted file mode 100644 index 4aa909e03..000000000 --- a/swift/Samples/Tutorial/Frameworks/TutorialBase/Sources/Todo/Model/TodoModel.swift +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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. - */ - -struct TodoModel: Equatable { - var title: String - var note: String -} diff --git a/swift/Samples/Tutorial/Frameworks/TutorialBase/Sources/TutorialContainerViewController.swift b/swift/Samples/Tutorial/Frameworks/TutorialBase/Sources/TutorialContainerViewController.swift deleted file mode 100644 index 3df618fb3..000000000 --- a/swift/Samples/Tutorial/Frameworks/TutorialBase/Sources/TutorialContainerViewController.swift +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 UIKit -import Workflow -import WorkflowUI - -public final class TutorialContainerViewController: UIViewController { - let containerViewController: UIViewController - - public init() { - // Show one of the sample view controllers, to demonstrate the provided views: - self.containerViewController = WelcomeSampleViewController() - - super.init(nibName: nil, bundle: nil) - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override public func viewDidLoad() { - super.viewDidLoad() - - view.backgroundColor = .white - - addChild(containerViewController) - view.addSubview(containerViewController.view) - containerViewController.didMove(toParent: self) - } - - override public func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - - containerViewController.view.frame = view.bounds - } -} diff --git a/swift/Samples/Tutorial/Frameworks/TutorialBase/Sources/Welcome/WelcomeSampleViewController.swift b/swift/Samples/Tutorial/Frameworks/TutorialBase/Sources/Welcome/WelcomeSampleViewController.swift deleted file mode 100644 index dbd871855..000000000 --- a/swift/Samples/Tutorial/Frameworks/TutorialBase/Sources/Welcome/WelcomeSampleViewController.swift +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 TutorialViews - -final class WelcomeSampleViewController: UIViewController { - let welcomeView: WelcomeView - - init() { - self.welcomeView = WelcomeView(frame: .zero) - super.init(nibName: nil, bundle: nil) - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - - view.addSubview(welcomeView) - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - - welcomeView.frame = view.bounds - } -} diff --git a/swift/Samples/Tutorial/Frameworks/TutorialBase/Tests/TutorialTests.swift b/swift/Samples/Tutorial/Frameworks/TutorialBase/Tests/TutorialTests.swift deleted file mode 100644 index f5b45e2a2..000000000 --- a/swift/Samples/Tutorial/Frameworks/TutorialBase/Tests/TutorialTests.swift +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 XCTest - -final class TutorialTests: XCTestCase { - func testPlaceholder() { - XCTAssertEqual(1, 1, "Placeholder test") - } -} diff --git a/swift/Samples/Tutorial/Frameworks/TutorialBase/TutorialBase.podspec b/swift/Samples/Tutorial/Frameworks/TutorialBase/TutorialBase.podspec deleted file mode 100644 index 64e177bc1..000000000 --- a/swift/Samples/Tutorial/Frameworks/TutorialBase/TutorialBase.podspec +++ /dev/null @@ -1,28 +0,0 @@ -Pod::Spec.new do |s| - s.name = 'TutorialBase' - s.version = '1.0.0.LOCAL' - s.summary = 'See the README.' - s.homepage = 'https://github.com/square/workflow' - s.license = 'Apache License, Version 2.0' - s.author = 'Square' - s.source = { git: 'Not Published', tag: "podify/#{s.version}" } - - # 1.7 is needed for `swift_versions` support - s.cocoapods_version = '>= 1.7.0' - - s.swift_versions = ['5.0'] - s.ios.deployment_target = '11.0' - - s.source_files = 'Sources/**/*.swift' - - s.dependency 'TutorialViews' - s.dependency 'Workflow' - s.dependency 'WorkflowUI' - s.dependency 'BackStackContainer' - - s.test_spec 'Tests' do |test_spec| - test_spec.source_files = 'Tests/**/*.swift' - - test_spec.framework = 'XCTest' - end -end diff --git a/swift/Samples/Tutorial/Frameworks/TutorialViews/Sources/TodoEditView.swift b/swift/Samples/Tutorial/Frameworks/TutorialViews/Sources/TodoEditView.swift deleted file mode 100644 index dd41dff91..000000000 --- a/swift/Samples/Tutorial/Frameworks/TutorialViews/Sources/TodoEditView.swift +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 UIKit - -public final class TodoEditView: UIView, UITextViewDelegate { - public var title: String { - didSet { - titleField.text = title - } - } - - public var note: String { - didSet { - noteField.text = note - } - } - - public var onTitleChanged: (String) -> Void - public var onNoteChanged: (String) -> Void - - let titleField: UITextField - let noteField: UITextView - - override public init(frame: CGRect) { - self.title = "" - self.note = "" - self.onTitleChanged = { _ in } - self.onNoteChanged = { _ in } - - self.titleField = UITextField(frame: .zero) - self.noteField = UITextView(frame: .zero) - - super.init(frame: frame) - - backgroundColor = .white - - titleField.textAlignment = .center - titleField.addTarget(self, action: #selector(titleDidChange(sender:)), for: .editingChanged) - titleField.layer.borderColor = UIColor.black.cgColor - titleField.layer.borderWidth = 1.0 - - noteField.delegate = self - noteField.layer.borderColor = UIColor.gray.cgColor - noteField.layer.borderWidth = 1.0 - - addSubview(titleField) - addSubview(noteField) - } - - @available(*, unavailable) - public required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override public func layoutSubviews() { - super.layoutSubviews() - - let titleHeight: CGFloat = 44.0 - let spacing: CGFloat = 8.0 - let widthInset: CGFloat = 8.0 - - var yOffset = bounds.minY - - titleField.frame = CGRect( - x: bounds.minX, - y: yOffset, - width: bounds.maxX, - height: titleHeight - ) - .insetBy(dx: widthInset, dy: 0.0) - - yOffset += titleHeight + spacing - - noteField.frame = CGRect( - x: bounds.minX, - y: yOffset, - width: bounds.maxX, - height: bounds.maxY - yOffset - ) - .insetBy(dx: widthInset, dy: 0.0) - } - - @objc private func titleDidChange(sender: UITextField) { - guard let titleText = sender.text else { - return - } - - onTitleChanged(titleText) - } - - // MARK: UITextFieldDelegate - - @objc public func textViewDidChange(_ textView: UITextView) { - guard textView === noteField else { - return - } - - guard let noteText = textView.text else { - return - } - - onNoteChanged(noteText) - } -} diff --git a/swift/Samples/Tutorial/Frameworks/TutorialViews/Sources/TodoListView.swift b/swift/Samples/Tutorial/Frameworks/TutorialViews/Sources/TodoListView.swift deleted file mode 100644 index 8a89c44fc..000000000 --- a/swift/Samples/Tutorial/Frameworks/TutorialViews/Sources/TodoListView.swift +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 UIKit - -public final class TodoListView: UIView, UITableViewDelegate, UITableViewDataSource { - public var todoList: [String] { - didSet { - tableView.reloadData() - } - } - - public var onTodoSelected: (Int) -> Void - - let titleLabel: UILabel - let tableView: UITableView - - override public init(frame: CGRect) { - self.todoList = [] - self.onTodoSelected = { _ in } - self.titleLabel = UILabel(frame: .zero) - self.tableView = UITableView() - - super.init(frame: frame) - - backgroundColor = .white - - titleLabel.text = "What do you have to do?" - titleLabel.textColor = .black - titleLabel.textAlignment = .center - - tableView.delegate = self - tableView.dataSource = self - - addSubview(titleLabel) - addSubview(tableView) - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: UIView - - override public func layoutSubviews() { - super.layoutSubviews() - - titleLabel.frame = CGRect( - x: bounds.minX, - y: bounds.minY, - width: bounds.maxX, - height: 44.0 - ) - - let yOffset = titleLabel.frame.maxY + 8.0 - - tableView.frame = CGRect( - x: bounds.minX, - y: yOffset, - width: bounds.maxX, - height: bounds.maxY - yOffset - ) - } - - // MARK: UITableViewDelegate, UITableViewDataSource - - public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return todoList.count - } - - public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = UITableViewCell() - - cell.textLabel?.text = todoList[indexPath.row] - - return cell - } - - public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - tableView.deselectRow(at: indexPath, animated: true) - - onTodoSelected(indexPath.row) - } -} diff --git a/swift/Samples/Tutorial/Frameworks/TutorialViews/Sources/WelcomeView.swift b/swift/Samples/Tutorial/Frameworks/TutorialViews/Sources/WelcomeView.swift deleted file mode 100644 index 940d2ed9e..000000000 --- a/swift/Samples/Tutorial/Frameworks/TutorialViews/Sources/WelcomeView.swift +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 UIKit - -public final class WelcomeView: UIView { - public var name: String { - didSet { - nameField.text = name - } - } - - public var onNameChanged: (String) -> Void - public var onLoginTapped: () -> Void - - let welcomeLabel: UILabel - let nameField: UITextField - let button: UIButton - - override public init(frame: CGRect) { - self.name = "" - self.onNameChanged = { _ in } - self.onLoginTapped = {} - - self.welcomeLabel = UILabel(frame: .zero) - self.nameField = UITextField(frame: .zero) - self.button = UIButton(frame: .zero) - - super.init(frame: frame) - - welcomeLabel.text = "Welcome! Please Enter Your Name" - welcomeLabel.textAlignment = .center - - nameField.backgroundColor = UIColor(white: 0.92, alpha: 1.0) - nameField.addTarget(self, action: #selector(textDidChange(sender:)), for: .editingChanged) - - button.backgroundColor = UIColor(red: 41 / 255, green: 150 / 255, blue: 204 / 255, alpha: 1.0) - button.setTitle("Login", for: .normal) - button.addTarget(self, action: #selector(buttonTapped(sender:)), for: .touchUpInside) - - addSubview(welcomeLabel) - addSubview(nameField) - addSubview(button) - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override public func layoutSubviews() { - super.layoutSubviews() - - let inset: CGFloat = 12.0 - let height: CGFloat = 44.0 - var yOffset = (bounds.size.height - (2 * height + inset)) / 2.0 - - welcomeLabel.frame = CGRect( - x: bounds.origin.x, - y: bounds.origin.y, - width: bounds.size.width, - height: yOffset - ) - - nameField.frame = CGRect( - x: bounds.origin.x, - y: yOffset, - width: bounds.size.width, - height: height - ) - .insetBy(dx: inset, dy: 0.0) - - yOffset += height + inset - button.frame = CGRect( - x: bounds.origin.x, - y: yOffset, - width: bounds.size.width, - height: height - ) - .insetBy(dx: inset, dy: 0.0) - } - - @objc private func textDidChange(sender: UITextField) { - guard let text = sender.text else { - return - } - onNameChanged(text) - } - - @objc private func buttonTapped(sender: UIButton) { - onLoginTapped() - } -} diff --git a/swift/Samples/Tutorial/Frameworks/TutorialViews/TutorialViews.podspec b/swift/Samples/Tutorial/Frameworks/TutorialViews/TutorialViews.podspec deleted file mode 100644 index 7eaa46995..000000000 --- a/swift/Samples/Tutorial/Frameworks/TutorialViews/TutorialViews.podspec +++ /dev/null @@ -1,17 +0,0 @@ -Pod::Spec.new do |s| - s.name = 'TutorialViews' - s.version = '1.0.0.LOCAL' - s.summary = 'See the README.' - s.homepage = 'https://github.com/square/workflow' - s.license = 'Apache License, Version 2.0' - s.author = 'Square' - s.source = { git: 'Not Published', tag: "podify/#{s.version}" } - - # 1.7 is needed for `swift_versions` support - s.cocoapods_version = '>= 1.7.0' - - s.swift_versions = ['5.0'] - s.ios.deployment_target = '11.0' - - s.source_files = 'Sources/**/*.swift' -end diff --git a/swift/Samples/Tutorial/Podfile b/swift/Samples/Tutorial/Podfile deleted file mode 100644 index 9e9aa2e6f..000000000 --- a/swift/Samples/Tutorial/Podfile +++ /dev/null @@ -1,20 +0,0 @@ -project 'Tutorial.xcodeproj' -platform :ios, '11.0' - -target 'Tutorial' do - pod 'Workflow', path: '../../../Workflow.podspec', :testspecs => ['Tests'] - pod 'WorkflowUI', path: '../../../WorkflowUI.podspec', :testspecs => ['Tests'] - pod 'BackStackContainer', path: '../BackStackContainer/BackStackContainer.podspec' - - pod 'TutorialViews', path: 'Frameworks/TutorialViews/TutorialViews.podspec' - pod 'TutorialBase', path: 'Frameworks/TutorialBase/TutorialBase.podspec', :testspecs => ['Tests'] - pod 'Tutorial1', path: 'Frameworks/Tutorial1Complete/Tutorial1.podspec' - pod 'Tutorial2', path: 'Frameworks/Tutorial2Complete/Tutorial2.podspec' - pod 'Tutorial3', path: 'Frameworks/Tutorial3Complete/Tutorial3.podspec' - pod 'Tutorial4', path: 'Frameworks/Tutorial4Complete/Tutorial4.podspec', :testspecs => ['Tests'] - pod 'Tutorial5', path: 'Frameworks/Tutorial5Complete/Tutorial5.podspec', :testspecs => ['Tests'] -end - -target 'TutorialTests' do - pod 'WorkflowTesting', path: '../../../WorkflowTesting.podspec', :testspecs => ['Tests'] -end diff --git a/swift/Samples/Tutorial/README.md b/swift/Samples/Tutorial/README.md deleted file mode 100644 index 08ac10c21..000000000 --- a/swift/Samples/Tutorial/README.md +++ /dev/null @@ -1,29 +0,0 @@ -# Tutorial - -## Overview - -Oh hi! Looks like you want build some software with Workflows! It's a bit different from traditional iOS development, so let's go through building a simple little TODO app to get the basics down. - -## Layout - -The project has both a starting point, as well as an example of the completed tutorial. - -Nearly all of the software is in cocoapods under the `Frameworks` directory. - -To help with the setup, there are already created a few helpers: -- `TutorialViews`: A set of 3 views for the 3 screens we will be building, `Welcome`, `TodoList`, and `TodoEdit`. -- `TutorialBase`: This is the starting point to build out the tutorial. It contains view controllers that host the views from `TutorialViews` to see how they display. - - Additionally, there is a `TutorialContainerViewController` that the AppDelegate sets as the root view controller. This will be our launching point for all of our workflows. -- `TutorialFinal`: This is an example of the completed tutorial - could be used as a reference if you get stuck. - -## Getting up and running - -The tutorial uses cocoapods as the dependency management. To get set up, run `bundle install`, then `bundle exec pod install`. Open `Tutorial.xcworkspace`. - -# Tutorial Steps - -- [Tutorial 1](Tutorial1.md) - Single view backed by a workflow -- [Tutorial 2](Tutorial2.md) - Multiple views and navigation -- [Tutorial 3](Tutorial3.md) - State throughout a tree of workflows -- [Tutorial 4](Tutorial4.md) - Refactoring -- [Tutorial 5](Tutorial5.md) - Testing diff --git a/swift/Samples/Tutorial/Tutorial.xcodeproj/project.pbxproj b/swift/Samples/Tutorial/Tutorial.xcodeproj/project.pbxproj deleted file mode 100644 index 11cac14fa..000000000 --- a/swift/Samples/Tutorial/Tutorial.xcodeproj/project.pbxproj +++ /dev/null @@ -1,571 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 51; - objects = { - -/* Begin PBXBuildFile section */ - 57AD55D94D909BD627910D7B /* libPods-TutorialTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5EF26974165B129BC6DE52E1 /* libPods-TutorialTests.a */; }; - E85390AB2314AF2D001B6313 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E85390AA2314AF2D001B6313 /* AppDelegate.swift */; }; - E85390B22314AF2E001B6313 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E85390B12314AF2E001B6313 /* Assets.xcassets */; }; - E85390B52314AF2E001B6313 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E85390B32314AF2E001B6313 /* LaunchScreen.storyboard */; }; - E8907A49231F162B00F1BB2E /* TutorialTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8907A48231F162B00F1BB2E /* TutorialTests.swift */; }; - F77B18B3E43BEC102E47DEE6 /* libPods-Tutorial.a in Frameworks */ = {isa = PBXBuildFile; fileRef = A115B9BFAF3DE7B7D706ACB7 /* libPods-Tutorial.a */; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - E8907A4B231F162B00F1BB2E /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = E853909F2314AF2D001B6313 /* Project object */; - proxyType = 1; - remoteGlobalIDString = E85390A62314AF2D001B6313; - remoteInfo = Tutorial; - }; -/* End PBXContainerItemProxy section */ - -/* Begin PBXFileReference section */ - 5EF26974165B129BC6DE52E1 /* libPods-TutorialTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-TutorialTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; - 89B9987CFE631DF2BA454704 /* Pods-Tutorial.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Tutorial.release.xcconfig"; path = "Target Support Files/Pods-Tutorial/Pods-Tutorial.release.xcconfig"; sourceTree = ""; }; - A115B9BFAF3DE7B7D706ACB7 /* libPods-Tutorial.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Tutorial.a"; sourceTree = BUILT_PRODUCTS_DIR; }; - BAD7C45E831F2461DD722CB8 /* Pods-Tutorial.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Tutorial.debug.xcconfig"; path = "Target Support Files/Pods-Tutorial/Pods-Tutorial.debug.xcconfig"; sourceTree = ""; }; - CB24A23238CDD92FC7B1717B /* Pods-TutorialTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TutorialTests.release.xcconfig"; path = "Target Support Files/Pods-TutorialTests/Pods-TutorialTests.release.xcconfig"; sourceTree = ""; }; - DB10D9654B0E3B8B61678123 /* Pods-TutorialTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TutorialTests.debug.xcconfig"; path = "Target Support Files/Pods-TutorialTests/Pods-TutorialTests.debug.xcconfig"; sourceTree = ""; }; - E85390A72314AF2D001B6313 /* Tutorial.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Tutorial.app; sourceTree = BUILT_PRODUCTS_DIR; }; - E85390AA2314AF2D001B6313 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - E85390B12314AF2E001B6313 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - E85390B42314AF2E001B6313 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - E85390B62314AF2E001B6313 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - E8907A46231F162B00F1BB2E /* TutorialTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TutorialTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - E8907A48231F162B00F1BB2E /* TutorialTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TutorialTests.swift; sourceTree = ""; }; - E8907A4A231F162B00F1BB2E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - E85390A42314AF2D001B6313 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - F77B18B3E43BEC102E47DEE6 /* libPods-Tutorial.a in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - E8907A43231F162B00F1BB2E /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 57AD55D94D909BD627910D7B /* libPods-TutorialTests.a in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 388F8A674EF339ECE9A5D046 /* Frameworks */ = { - isa = PBXGroup; - children = ( - A115B9BFAF3DE7B7D706ACB7 /* libPods-Tutorial.a */, - 5EF26974165B129BC6DE52E1 /* libPods-TutorialTests.a */, - ); - name = Frameworks; - sourceTree = ""; - }; - 96383393E09F25B5773BDA71 /* Pods */ = { - isa = PBXGroup; - children = ( - BAD7C45E831F2461DD722CB8 /* Pods-Tutorial.debug.xcconfig */, - 89B9987CFE631DF2BA454704 /* Pods-Tutorial.release.xcconfig */, - DB10D9654B0E3B8B61678123 /* Pods-TutorialTests.debug.xcconfig */, - CB24A23238CDD92FC7B1717B /* Pods-TutorialTests.release.xcconfig */, - ); - path = Pods; - sourceTree = ""; - }; - E853909E2314AF2D001B6313 = { - isa = PBXGroup; - children = ( - E85390BE2314B001001B6313 /* Configuration */, - E85390BD2314AFE7001B6313 /* Resources */, - E85390BC2314AFC8001B6313 /* Sources */, - E8907A47231F162B00F1BB2E /* TutorialTests */, - E85390A82314AF2D001B6313 /* Products */, - 96383393E09F25B5773BDA71 /* Pods */, - 388F8A674EF339ECE9A5D046 /* Frameworks */, - ); - sourceTree = ""; - }; - E85390A82314AF2D001B6313 /* Products */ = { - isa = PBXGroup; - children = ( - E85390A72314AF2D001B6313 /* Tutorial.app */, - E8907A46231F162B00F1BB2E /* TutorialTests.xctest */, - ); - name = Products; - sourceTree = ""; - }; - E85390BC2314AFC8001B6313 /* Sources */ = { - isa = PBXGroup; - children = ( - E85390AA2314AF2D001B6313 /* AppDelegate.swift */, - ); - name = Sources; - path = AppHost/Sources; - sourceTree = ""; - }; - E85390BD2314AFE7001B6313 /* Resources */ = { - isa = PBXGroup; - children = ( - E85390B12314AF2E001B6313 /* Assets.xcassets */, - E85390B32314AF2E001B6313 /* LaunchScreen.storyboard */, - ); - name = Resources; - path = AppHost/Resources; - sourceTree = ""; - }; - E85390BE2314B001001B6313 /* Configuration */ = { - isa = PBXGroup; - children = ( - E85390B62314AF2E001B6313 /* Info.plist */, - ); - name = Configuration; - path = AppHost/Configuration; - sourceTree = ""; - }; - E8907A47231F162B00F1BB2E /* TutorialTests */ = { - isa = PBXGroup; - children = ( - E8907A48231F162B00F1BB2E /* TutorialTests.swift */, - E8907A4A231F162B00F1BB2E /* Info.plist */, - ); - name = TutorialTests; - path = AppHost/TutorialTests; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - E85390A62314AF2D001B6313 /* Tutorial */ = { - isa = PBXNativeTarget; - buildConfigurationList = E85390B92314AF2E001B6313 /* Build configuration list for PBXNativeTarget "Tutorial" */; - buildPhases = ( - 7445F921FF925E3732A5F2CD /* [CP] Check Pods Manifest.lock */, - E85390A32314AF2D001B6313 /* Sources */, - E85390A42314AF2D001B6313 /* Frameworks */, - E85390A52314AF2D001B6313 /* Resources */, - E437DBFEAF9DDB6255B08476 /* [CP] Copy Pods Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = Tutorial; - productName = Tutorial; - productReference = E85390A72314AF2D001B6313 /* Tutorial.app */; - productType = "com.apple.product-type.application"; - }; - E8907A45231F162B00F1BB2E /* TutorialTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = E8907A4F231F162B00F1BB2E /* Build configuration list for PBXNativeTarget "TutorialTests" */; - buildPhases = ( - F31716859C0BC0B7AA34707F /* [CP] Check Pods Manifest.lock */, - E8907A42231F162B00F1BB2E /* Sources */, - E8907A43231F162B00F1BB2E /* Frameworks */, - E8907A44231F162B00F1BB2E /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - E8907A4C231F162B00F1BB2E /* PBXTargetDependency */, - ); - name = TutorialTests; - productName = TutorialTests; - productReference = E8907A46231F162B00F1BB2E /* TutorialTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - E853909F2314AF2D001B6313 /* Project object */ = { - isa = PBXProject; - attributes = { - LastSwiftUpdateCheck = 1020; - LastUpgradeCheck = 1020; - ORGANIZATIONNAME = Square; - TargetAttributes = { - E85390A62314AF2D001B6313 = { - CreatedOnToolsVersion = 10.2.1; - }; - E8907A45231F162B00F1BB2E = { - CreatedOnToolsVersion = 10.2.1; - TestTargetID = E85390A62314AF2D001B6313; - }; - }; - }; - buildConfigurationList = E85390A22314AF2D001B6313 /* Build configuration list for PBXProject "Tutorial" */; - compatibilityVersion = "Xcode 9.3"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = E853909E2314AF2D001B6313; - productRefGroup = E85390A82314AF2D001B6313 /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - E85390A62314AF2D001B6313 /* Tutorial */, - E8907A45231F162B00F1BB2E /* TutorialTests */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - E85390A52314AF2D001B6313 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - E85390B52314AF2E001B6313 /* LaunchScreen.storyboard in Resources */, - E85390B22314AF2E001B6313 /* Assets.xcassets in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - E8907A44231F162B00F1BB2E /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 7445F921FF925E3732A5F2CD /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Tutorial-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; - E437DBFEAF9DDB6255B08476 /* [CP] Copy Pods Resources */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Tutorial/Pods-Tutorial-resources-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Copy Pods Resources"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Tutorial/Pods-Tutorial-resources-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Tutorial/Pods-Tutorial-resources.sh\"\n"; - showEnvVarsInLog = 0; - }; - F31716859C0BC0B7AA34707F /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-TutorialTests-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - E85390A32314AF2D001B6313 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - E85390AB2314AF2D001B6313 /* AppDelegate.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - E8907A42231F162B00F1BB2E /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - E8907A49231F162B00F1BB2E /* TutorialTests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - E8907A4C231F162B00F1BB2E /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = E85390A62314AF2D001B6313 /* Tutorial */; - targetProxy = E8907A4B231F162B00F1BB2E /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - -/* Begin PBXVariantGroup section */ - E85390B32314AF2E001B6313 /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - E85390B42314AF2E001B6313 /* Base */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - E85390B72314AF2E001B6313 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - CODE_SIGN_IDENTITY = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.2; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - }; - name = Debug; - }; - E85390B82314AF2E001B6313 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - CODE_SIGN_IDENTITY = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.2; - MTL_ENABLE_DEBUG_INFO = NO; - MTL_FAST_MATH = YES; - SDKROOT = iphoneos; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - E85390BA2314AF2E001B6313 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = BAD7C45E831F2461DD722CB8 /* Pods-Tutorial.debug.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CODE_SIGN_STYLE = Automatic; - INFOPLIST_FILE = "$(SRCROOT)/AppHost/Configuration/Info.plist"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.squareup.Tutorial; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - E85390BB2314AF2E001B6313 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 89B9987CFE631DF2BA454704 /* Pods-Tutorial.release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CODE_SIGN_STYLE = Automatic; - INFOPLIST_FILE = "$(SRCROOT)/AppHost/Configuration/Info.plist"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.squareup.Tutorial; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Release; - }; - E8907A4D231F162B00F1BB2E /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = DB10D9654B0E3B8B61678123 /* Pods-TutorialTests.debug.xcconfig */; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - INFOPLIST_FILE = AppHost/TutorialTests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.squareup.TutorialTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Tutorial.app/Tutorial"; - }; - name = Debug; - }; - E8907A4E231F162B00F1BB2E /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = CB24A23238CDD92FC7B1717B /* Pods-TutorialTests.release.xcconfig */; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - INFOPLIST_FILE = AppHost/TutorialTests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.squareup.TutorialTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Tutorial.app/Tutorial"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - E85390A22314AF2D001B6313 /* Build configuration list for PBXProject "Tutorial" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - E85390B72314AF2E001B6313 /* Debug */, - E85390B82314AF2E001B6313 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - E85390B92314AF2E001B6313 /* Build configuration list for PBXNativeTarget "Tutorial" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - E85390BA2314AF2E001B6313 /* Debug */, - E85390BB2314AF2E001B6313 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - E8907A4F231F162B00F1BB2E /* Build configuration list for PBXNativeTarget "TutorialTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - E8907A4D231F162B00F1BB2E /* Debug */, - E8907A4E231F162B00F1BB2E /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = E853909F2314AF2D001B6313 /* Project object */; -} diff --git a/swift/Samples/Tutorial/Tutorial.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/swift/Samples/Tutorial/Tutorial.xcodeproj/project.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index b1806ffae..000000000 --- a/swift/Samples/Tutorial/Tutorial.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/swift/Samples/Tutorial/Tutorial.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/swift/Samples/Tutorial/Tutorial.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d981003..000000000 --- a/swift/Samples/Tutorial/Tutorial.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/swift/Samples/Tutorial/Tutorial.xcodeproj/xcshareddata/xcschemes/Tutorial.xcscheme b/swift/Samples/Tutorial/Tutorial.xcodeproj/xcshareddata/xcschemes/Tutorial.xcscheme deleted file mode 100644 index 63caa5f93..000000000 --- a/swift/Samples/Tutorial/Tutorial.xcodeproj/xcshareddata/xcschemes/Tutorial.xcscheme +++ /dev/null @@ -1,131 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/swift/Samples/Tutorial/Tutorial.xcworkspace/contents.xcworkspacedata b/swift/Samples/Tutorial/Tutorial.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index c96d59c6f..000000000 --- a/swift/Samples/Tutorial/Tutorial.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - diff --git a/swift/Samples/Tutorial/Tutorial.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/swift/Samples/Tutorial/Tutorial.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d981003..000000000 --- a/swift/Samples/Tutorial/Tutorial.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/swift/Samples/Tutorial/Tutorial1.md b/swift/Samples/Tutorial/Tutorial1.md deleted file mode 100644 index b5a32284a..000000000 --- a/swift/Samples/Tutorial/Tutorial1.md +++ /dev/null @@ -1,284 +0,0 @@ -# Step 1 - -_Let's get something on the screen..._ - -## Setup - -To follow this tutorial: -- Open your terminal and run `bundle exec pod install` in the `swift/Samples/Tutorial` directory. -- Open `Tutorial.xcworkspace` and build the `Tutorial` Scheme. - -The `TutorialBase` pod in `Frameworks` will be our starting place to build from. - -The welcome screen should look like: - -![Welcome](images/welcome.png) - -You can enter a name, but the login button won't do anything. - -## First Workflow - -Let's start by making a workflow and screen to back the welcome view. - -Start by creating a new workflow and screen by creating a new file with the [Xcode templates](../../Tooling/Templates/install-xcode-templates.sh), adding it to the `TutorialBase` target: - -![New Workflow](images/new-workflow.png) -![Workflow Name](images/workflow-name.png) -![File Location](images/workflow-file-location.png) - -Follow the same steps using the `Screen (View Controller)` template. We can delete the `WelcomeSampleViewController.swift` file in the base tutorial, as we'll be replacing it. - -### Screens and View Controllers - -Let's start with what a `Screen` is, and how it relates to the view controller. - -The `Screen` protocol is a marker protocol, intended to describe the view model that will be used to drive a view controller. - -For out welcome screen, we'll define what it needs for a backing view model: -```swift -struct WelcomeScreen: Screen { - /// The current name that has been entered. - var name: String - /// Callback when the name changes in the UI. - var onNameChanged: (String) -> Void - /// Callback when the login button is tapped. - var onLoginTapped: () -> Void - - func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { - return WelcomeViewController.description(for: self, environment: environment) - } -} -``` - -Now add the (convenient) `WelcomeView` to our view controller (if you would like to create and layout the view yourself, feel free to do it!). Add a `welcomeView` property to the view controller, and add and lay it out in `viewDidLoad` and `viewDidLayoutSubviews` respectively. -```swift -// Import the `TutorialViews` module for the `WelcomeView` -import TutorialViews - -final class WelcomeViewController: ScreenViewController { - var welcomeView: WelcomeView - - required init(screen: WelcomeScreen) { - self.welcomeView = WelcomeView(frame: .zero) - super.init(screen: screen) - update(with: screen) - } - - override func viewDidLoad() { - super.viewDidLoad() - - view.addSubview(welcomeView) - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - - welcomeView.frame = view.bounds.inset(by: view.safeAreaInsets) - } -``` - -The screen is passed into the view controller when it is initialized, as well as `screenDidChange` being called anytime the back screen is updated. The template provides a single method to handle updates to the screen, so fill that in now: -```swift - private func update(with screen: WelcomeScreen) { - /// Update UI - welcomeView.name = screen.name - welcomeView.onNameChanged = screen.onNameChanged - welcomeView.onLoginTapped = screen.onLoginTapped - } -``` - -Any time the screen is updated, the `WelcomeViewController` will update the `name` and `onNameChanged` fields on the `WelcomeView`. We can't quite run yet, as we still need to fill in the basics of our workflow. - -### Workflows and Rendering Type - -The core responsibility of a workflow is to provide a "rendering" every time the related state updates. Let's go into the `WelcomeWorkflow` now, and have it return a `WelcomeScreen` in the `render` method. - -```swift -// MARK: Rendering - -extension WelcomeWorkflow { - typealias Rendering = WelcomeScreen - - func render(state: WelcomeWorkflow.State, context: RenderContext) -> Rendering { - return WelcomeScreen( - name: "", - onNameChanged: { _ in - }, - onLoginTapped: { - }) - } -} -``` - -### Setting up the ContainerViewController - -Now we have our `WelcomeWorkflow` rendering a `WelcomeScreen`, and have a view controller that knows how to display with a `WelcomeScreen`. It's time to bind this all together and get it displaying. - -We'll update the `TutorialContainerViewController` to hold a child *ContainerViewController* that will host our workflow: - -```swift -import UIKit -import Workflow -import WorkflowUI - - -public final class TutorialContainerViewController: UIViewController { - let containerViewController: UIViewController - - public init() { - // Create a `ContainerViewController` with the `WelcomeWorkflow` as the root workflow. - containerViewController = ContainerViewController( - workflow: WelcomeWorkflow() - ) - - super.init(nibName: nil, bundle: nil) - } -``` - -Now, we've created our `ContainerViewController` with the `WelcomeWorkflow` as the root. - -We can finally run the app again! It will look the exact same as before, but now powered by our workflow. - -## Driving the UI from Workflow State - -Right now, the workflow isn't handling any of the events from the UI. Let's update it to be responsible for the login name as well as the action when the login button is pressed. - -### State - -All workflows have a `State` type that represents the internal state of the workflow. This should be all of the data that this workflow is _responsible_ for - usually corresponds to the state for the UI. - -We will model the first part of state that we want to track, the login `name`. Update the `State` type to include a name. We will also need to update `makeInitialState` to give an initial value: -```swift -// MARK: State and Initialization - -extension WelcomeWorkflow { - - struct State { - var name: String - } - - func makeInitialState() -> WelcomeWorkflow.State { - return State(name: "") - } - - // ... -``` - -Now that we have the state modelled, we'll send it to the UI every time a render pass happens - the text field will overwrite it's value with whatever was provided. - -```swift -// MARK: Rendering - -extension WelcomeWorkflow { - - typealias Rendering = WelcomeScreen - - func render(state: WelcomeWorkflow.State, context: RenderContext) -> Rendering { - return WelcomeScreen( - name: state.name, - onNameChanged: { _ in - }, - onLoginTapped: { - }) - } -} -``` - -If you run the app again, you'll see that it still behaves the same, letting your type into the name field. This is because we only have rendered a screen once. - -To update the workflow's internal state, we need to add an "Action": - -### Actions - -Actions define how a workflow handles events received from the outside world, like UI events (such a button presses), network requests, data stores, etc. Generally an `Action` type is an enum which make it easy to define all of the actions that this workflow will handle. - -Add a case to the existing `Action` called `nameChanged` to update our internal state: -```swift -// MARK: Actions - -extension WelcomeWorkflow { - - enum Action: WorkflowAction { - - typealias WorkflowType = WelcomeWorkflow - - case nameChanged(name: String) - - func apply(toState state: inout WelcomeWorkflow.State) -> WelcomeWorkflow.Output? { - - switch self { - - case .nameChanged(name: let name): - // Update our state with the updated name. - state.name = name - // Return `nil` for the output, we want to handle this action only at the level of this workflow. - return nil - } - } - } - -} -``` - -We need to send this action back to the workflow any time the name changes. Update the `render` method to send it through a sink back to the workflow whenever the `onNameChanged` closure is called: - -```swift -// MARK: Rendering - -extension WelcomeWorkflow { - - typealias Rendering = WelcomeScreen - - func render(state: WelcomeWorkflow.State, context: RenderContext) -> Rendering { - // Create a "sink" of type `Action`. A sink is what we use to send actions to the workflow. - let sink = context.makeSink(of: Action.self) - - return WelcomeScreen( - name: state.name, - onNameChanged: { name in - sink.send(.nameChanged(name: name)) - }, - onLoginTapped: { - }) - } -} -``` - -### The update loop - -If we run the app again, it will still behave the same but we are now capturing the name changes in our workflow's state, as well as having the UI show the name based upon the workflows internal state. - -To see this, change the `apply` method to append an extra letter on the name received, eg: -```swift - func apply(toState state: inout WelcomeWorkflow.State) -> WelcomeWorkflow.Output? { - - switch self { - case .nameChanged(name: let name): - // Update our state with the updated name. - state.name = name + "a" - // Return `nil` for the output, we want to handle this action only at the level of this workflow. - return nil - } - } -``` - -Running the app again will have the name field suffixed with a letter 'a' on every keypress. We probably want to undo this change, but it demonstrates that the UI is being updated from the internal state. - -Here is what is happening on each keypress: -1) The UI called `onNameChanged` whenever the contents of the text field change. -2) The closure calls `sink.send(.nameChanged(name: name)`, which sends an action to be handled by the workflow. -3) The `apply` method on the action is called. The `state` parameter is an `inout` parameter, so when it is updated in `apply`, it updates the actual state. - - This is effectively the same as this method being written `func apply(fromState: State) -> (State, Output?)` where it transforms the previous state into a new state. -4) As an action was just handled, the workflow must now be re-rendered so the `Screen` (and from it, the UI) can be updated. - - `render` is called on the workflow. A new screen is returned with the updated `name` from the internal state. -5) The view controller is provided the new screen with the call to `func screenDidChange(from previousScreen: WelcomeScreen)`. Generally, the update is handled in the convenience method on the template of `private func update(with screen: WelcomeScreen)`. - - This view controller updates the text field with the received name value, and also updates the callbacks for when the name changes or login is pressed. -6) The workflow waits for the next Action to be received, and then the goes through the same update loop. - -# Summary - -In this tutorial, we covered creating a Screen, ScreenViewController, Workflow, and binding them together in a ContainerViewController. We also covered the Workflow being responsible for the state of the UI instead of the view controller being responsible. - -Next, we will create a second screen and workflow, and the use composition to navigate between them. - -[Tutorial 2](Tutorial2.md) diff --git a/swift/Samples/Tutorial/Tutorial2.md b/swift/Samples/Tutorial/Tutorial2.md deleted file mode 100644 index 4115405f3..000000000 --- a/swift/Samples/Tutorial/Tutorial2.md +++ /dev/null @@ -1,589 +0,0 @@ -# Step 2 - -_Multiple Screens and Navigation_ - -## Setup - -To follow this tutorial: -- Open your terminal and run `bundle exec pod install` in the `swift/Samples/Tutorial` directory. -- Open `Tutorial.xcworkspace` and build the `Tutorial` Scheme. - -Start from implementation of `Tutorial1` if you're skipping ahead. You can run this by updating the `AppDelegate` to import `Tutorial1` instead of `TutorialBase`. - -## Second Workflow - -Let's add a second screen and workflow so we have somewhere to land after we finish login. Our next screen will be a list of "TODO" items, as TODO apps are the best apps. To see an example, modify the `TutorialContainerViewController` to show the `TodoListSampleViewController`. It can be removed, as we will be replacing it with a screen and workflow. - -Create a new Screen/ViewController pair called `TodoList`: - -![New Screen](images/new-screen.png) -![TodoListScreen](images/new-screen-todolist.png) - -Add the provided `TodoListView` from `TutorialViews` as a subview to the newly created view controller: - -```swift -import TutorialViews - - -struct TodoListScreen: Screen { - // This should contain all data to display in the UI - - // It should also contain callbacks for any UI events, for example: - // var onButtonTapped: () -> Void - - // It should also return viewControllerDescription property that - // describes the UIViewController that will be used for rendering - // the screen. -} - - -final class TodoListViewController: ScreenViewController { - let todoListView: TodoListView - - required init(screen: TodoListScreen) { - self.todoListView = TodoListView(frame: .zero) - super.init(screen: screen) - update(with: screen) - } - - override func viewDidLoad() { - super.viewDidLoad() - - view.addSubview(todoListView) - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - - todoListView.frame = view.bounds.inset(by: view.safeAreaInsets) - } - // The rest of the view controller... -``` - -And then create the corresponding workflow called "TodoList": - -![New TodoList Workflow](images/new-todolist-workflow.png) - -Modify the rendering to return a `TodoListScreen`, we can leave everything else as the default for now: - -```swift -// MARK: Rendering - -extension TodoListWorkflow { - - typealias Rendering = TodoListScreen - - func render(state: TodoListWorkflow.State, context: RenderContext) -> Rendering { - return TodoListScreen() - } -} -``` - -### Showing the new screen and workflow - -For now, let's just show this new screen instead of the login screen/workflow. Update the `TutorialContainerViewController` to show the `TodoListWorkflow`: - -```swift -public final class TutorialContainerViewController: UIViewController { - let containerViewController: UIViewController - - public init() { - // Create a `ContainerViewController` with the `WelcomeWorkflow` as the root workflow. -// containerViewController = ContainerViewController( -// workflow: WelcomeWorkflow() -// ) - // Show the TodoList Workflow instead: - containerViewController = ContainerViewController( - workflow: TodoListWorkflow() - ) - - super.init(nibName: nil, bundle: nil) - } -``` - -Run the app again, and now the empty todo list (table view) will be shown: - -![Empty Todo List](images/empty-todolist.png) - -## Populating the Todo List - -The empty list is rather boring, so let's fill it in with some sample data for now. Update the `State` type to include a list of todo model objects and change `makeInitialState` to include a default one: - -```swift -// TodoListWorkflow.swift - -// MARK: State and Initialization - -extension TodoListWorkflow { - - struct State { - var todos: [TodoModel] - } - - func makeInitialState() -> TodoListWorkflow.State { - return State(todos: [TodoModel(title: "Take the cat for a walk", note: "Cats really need their outside sunshine time. Don't forget to walk Charlie. Hamilton is less excited about the prospect.")]) - } - - func workflowDidChange(from previousWorkflow: TodoListWorkflow, state: inout State) { - - } -} -``` - -Add a `todoTitles` property to the `TodoScreen`, and change `update` to update the `TodoListView` to change what it shows anytime the screen updates: - -```swift -struct TodoListScreen: Screen { - // The titles of the todo items - var todoTitles: [String] - - // Callback when a todo is selected - var onTodoSelected: (Int) -> Void -} - -final class TodoListViewController: ScreenViewController { - // ...snipped... - - override func screenDidChange(from previousScreen: TodoListScreen) { - update(with: screen) - } - - private func update(with screen: TodoListScreen) { - // Update the todoList on the view with what the screen provided: - todoListView.todoList = screen.todoTitles - todoListView.onTodoSelected = screen.onTodoSelected - } - -} - -``` - -Finally, update the `render` for `TodoListWorkflow` to send the titles of the todo models whenever the screen is updated: - -```swift -// MARK: Rendering - -extension TodoListWorkflow { - - typealias Rendering = TodoListScreen - - func render(state: TodoListWorkflow.State, context: RenderContext) -> Rendering { - let titles = state.todos.map { (todoModel) -> String in - return todoModel.title - } - return TodoListScreen( - todoTitles: titles, - onTodoSelected: { _ in }) - } -} -``` - -Run the app again, and now there should be a single visible item in the list: - -![Todo list hard coded](images/tut2-todolist-example.png) - -## Composition and Navigation - -Now that there are two different screens, we can make our first workflow showing composition with a single parent and two child workflows. Our `WelcomeWorkflow` and `TodoListWorkflow` will be the leaf nodes with a new workflow as the root. - -### Root Workflow - -Create a new workflow called `Root` with the templates. - -We'll start with the `RootWorkflow` returning only showing the `WelcomeScreen` via the `WelcomeWorkflow`. Updated the `Rendering` typealias and `render` to have the `RootWorkflow` defer to a child: - -```swift -// MARK: Rendering - -extension RootWorkflow { - - typealias Rendering = WelcomeScreen - - func render(state: RootWorkflow.State, context: RenderContext) -> Rendering { - // Render a child workflow of type `WelcomeWorkflow`. When `rendered(with:)` is called, the infrastructure will create - // a child workflow with state if one is not already running. - let welcomeScreen = WelcomeWorkflow() - .rendered(with: context) - - return welcomeScreen - } -} -``` - -However, this won't compile immediately, and the compiler will provide a less than useful error message: - -![missing-map-output](images/missing-map-output.png) - -Anytime a child workflow is run, the parent needs a way of converting its `Output` into an `Action` it can handle. The `WelcomeWorkflow`'s output type is currently an empty enum: `enum Output { }`. - -For now, delete the `Output` on `WelcomeWorkflow` and replace it with a typealias to `Never`: - -```swift -struct WelcomeWorkflow: Workflow { - typealias Output = Never -} -``` - -Update the `TutorialContainerViewController` to start at the `RootWorkflow` and we'll see the welcome screen again: - -```swift -public final class TutorialContainerViewController: UIViewController { - let containerViewController: UIViewController - - public init() { - // ... - - // Create a `ContainerViewController` with the `RootWorkflow` as the root workflow. - containerViewController = ContainerViewController( - workflow: RootWorkflow() - ) - - super.init(nibName: nil, bundle: nil) - } - // ... -} -``` - -### Navigating between Workflows - -Now that there is a root workflow, it can be updated to navigate between the `Welcome` and `TodoList` workflows. - -Start by defining the state that needs to be tracked at the root - specifically which screen we're showing, and the actions to login and logout: - -```swift -// MARK: State and Initialization - -extension RootWorkflow { - - // The state is an enum, and can either be on the welcome screen or the todo list. - // When on the todo list, it also includes the name provided on the welcome screen - enum State { - // The welcome screen via the welcome workflow will be shown - case welcome - // The todo list screen via the todo list workflow will be shown. The name will be provided to the todo list. - case todo(name: String) - } - - func makeInitialState() -> RootWorkflow.State { - return .welcome - } - // ... -} -``` - -```swift -// MARK: Actions - -extension RootWorkflow { - - enum Action: WorkflowAction { - - typealias WorkflowType = RootWorkflow - - case login(name: String) - case logout - - func apply(toState state: inout RootWorkflow.State) -> RootWorkflow.Output? { - - switch self { - case .login(name: let name): - // When the `login` action is received, change the state to `todo`. - state = .todo(name: name) - case .logout: - // Return to the welcome state on logout. - state = .welcome - } - return nil - - } - } -} -``` - -The root workflow is now modeling our states and actions. Soon we will be able to navigate between the welcome and todo list screens. - -### Workflow Output - -Workflows can only communicate with each other through their "properties" as inputs and "outputs" as actions. When a child workflow emits an output, the parent workflow will receive it and map it into an action they can handle. - -Our welcome workflow has a login button that doesn't do anything, and we'll now handle it and let our parent know that we've "logged in" so it can navigate to another screen. - -Add an action for `didLogin` and define our `Output` type to be able to message our parent: - -```swift -// MARK: Actions - -extension WelcomeWorkflow { - - enum Action: WorkflowAction { - - typealias WorkflowType = WelcomeWorkflow - - case nameChanged(name: String) - case didLogin - - func apply(toState state: inout WelcomeWorkflow.State) -> WelcomeWorkflow.Output? { - - switch self { - case .nameChanged(name: let name): - // Update our state with the updated name. - state.name = name - // Return `nil` for the output, we want to handle this action only at the level of this workflow. - return nil - - case .didLogin: - // Return an output of `didLogin` with the name. - return .didLogin(name: state.name) - } - } - } -} -``` - -```swift -struct WelcomeWorkflow: Workflow { - enum Output { - case didLogin(name: String) - } -} -``` - -And fire the `.didLogin` action any time the login button is pressed: - -```swift -// MARK: Rendering - -extension WelcomeWorkflow { - - typealias Rendering = WelcomeScreen - - func render(state: WelcomeWorkflow.State, context: RenderContext) -> Rendering { - - // Create a "sink" of type `Action`. A sink is what we use to send actions to the workflow. - let sink = context.makeSink(of: Action.self) - - return WelcomeScreen( - name: state.name, - onNameChanged: { name in - sink.send(.nameChanged(name: name)) - }, - onLoginTapped: { - // Whenever the login button is tapped, emit the `.didLogin` action. - sink.send(.didLogin) - }) - } -} -``` - -Finally, map the output event from `WelcomeWorkflow` in `RootWorkflow` to the `login` action: - -```swift -// MARK: Rendering - -extension RootWorkflow { - - typealias Rendering = WelcomeScreen - - func render(state: RootWorkflow.State, context: RenderContext) -> Rendering { - // Render a child workflow of type `WelcomeWorkflow`. When `rendered(with:)` is called, the infrastructure will create - // a child workflow with state if one is not already running. - let welcomeScreen = WelcomeWorkflow() - .mapOutput({ output -> Action in - switch output { - // When `WelcomeWorkflow` emits `didLogin`, turn it into our `login` action. - case .didLogin(name: let name): - return .login(name: name) - } - }) - .rendered(with: context) - - return welcomeScreen - } -} -``` - -### Showing a different workflow from state - -Now we are handling the `Output` of `WelcomeWorkflow`, and updating the state to show the `Todo` screen. However, we still need to update our render method to defer to a different workflow. - -We'll update the `render` method to show either the `WelcomeWorkflow` or `TodoListWorkflow` depending on the state of `RootWorkflow` - -Temporarily define the `Output` of `TodoListWorkflow` as `Never` (we can only go forward!): - -```swift -// MARK: Input and Output - -struct TodoListWorkflow: Workflow { - typealias Output = Never -} -``` - -And update the `render` method of the `RootWorkflow`: - -```swift -// MARK: Rendering - -extension RootWorkflow { - - typealias Rendering = AnyScreen - - func render(state: RootWorkflow.State, context: RenderContext) -> Rendering { - switch state { - // When the state is `.welcome`, defer to the WelcomeWorkflow - case .welcome: - // Render a child workflow of type `WelcomeWorkflow`. When `rendered(with:)` is called, the infrastructure will create - // a child workflow with state if one is not already running. - let welcomeScreen = WelcomeWorkflow() - .mapOutput({ output -> Action in - switch output { - // When `WelcomeWorkflow` emits `didLogin`, turn it into our `login` action. - case .didLogin(name: let name): - return .login(name: name) - } - }) - .rendered(with: context) - - return AnyScreen(welcomeScreen) - - case .todo(name: let name): - // When the state is `.todo`, defer to the TodoListWorkflow. - let todoListScreen = TodoListWorkflow() - .rendered(with: context) - - return AnyScreen(todoListScreen) - } - - } -} -``` - -#### AnyScreen and type erasure - -The `Rendering` type of `RootWorkflow` was changed to `AnyScreen` from the `WelcomeScreen` to be able to show different screen types. This is needed as swift is strongly typed, and we are potentially returning different types. - -To accomplish this, there is a technique called "type erasure" that is used. Effectively, we wrap the real type into a type that hides the underlying type. - -On the infrastructure side, when we display the different screen types, the view controller is swapped out from `Welcome` to instead show the `TodoList`. - -This is the "escape hatch" for having a workflow show different screen types based on the state. - -This works, but with no animation between the two screens it's pretty unsatisfying. We'll fix that by using a different "container" to provide the missing transition animation. - -### Back Stack and "Containers" - -We want to put our different screens in a navigation controller. Because we want all of our navigation state to be declarative, we need to use the `BackStackContainer` to do this - by using the `BackStackScreen`: - -```swift -public struct BackStackScreen: Screen { - var items: [Item] - - public init(items: [BackStackScreen.Item]) { - self.items = items - } -} -``` - -The `BackStackScreen` contains a list of all screens in the back stack that are specified on each render pass. - -```swift -import UIKit -import Workflow -import WorkflowUI -import BackStackContainer - - -public final class TutorialContainerViewController: UIViewController { - let containerViewController: UIViewController - - public init() { - // Create a `ContainerViewController` with the `RootWorkflow` as the root workflow. - containerViewController = ContainerViewController( - workflow: RootWorkflow() - ) - - super.init(nibName: nil, bundle: nil) - } - // ... the rest of the implementation ... -``` - -And update the `RootWorkflow` to return a `BackStackScreen` with a list of back stack items: - -```swift -// Don't forget to import `BackStackContainer` to be able to use `BackStackScreen`. -import BackStackContainer - -// ...snipped... - -// MARK: Rendering - -extension RootWorkflow { - - typealias Rendering = BackStackScreen - - func render(state: RootWorkflow.State, context: RenderContext) -> Rendering { - // Create a sink to handle the back action from the TodoListWorkflow to logout. - let sink = context.makeSink(of: Action.self) - - // Our list of back stack items. Will always include the "WelcomeScreen". - var backStackItems: [BackStackScreen.Item] = [] - - let welcomeScreen = WelcomeWorkflow() - .mapOutput({ output -> Action in - switch output { - // When `WelcomeWorkflow` emits `didLogin`, turn it into our `login` action. - case .didLogin(name: let name): - return .login(name: name) - } - }) - .rendered(with: context) - - let welcomeBackStackItem = BackStackScreen.Item( - key: "welcome", - screen: welcomeScreen, - // Hide the navigation bar. - barVisibility: .hidden) - - // Always add the welcome back stack item. - backStackItems.append(welcomeBackStackItem) - - switch state { - // When the state is `.welcome`, defer to the WelcomeWorkflow. - case .welcome: - // We always add the welcome screen to the backstack, so this is a no op. - break - - // When the state is `.todo`, defer to the TodoListWorkflow. - case .todo(name: let name): - - let todoListScreen = TodoListWorkflow() - .rendered(with: context) - - let todoListBackStackItem = BackStackScreen.Item( - key: "todoList", - screen: todoListScreen, - // Specify the title, back button, and right button. - barContent: BackStackScreen.BarContent( - title: "Welcome \(name)", - // When `back` is pressed, emit the .logout action to return to the welcome screen. - leftItem: .button(.back(handler: { - sink.send(.logout) - })), - rightItem: .none)) - - // Add the TodoListScreen to our BackStackItems. - backStackItems.append(todoListBackStackItem) - } - - // Finally, return the BackStackScreen with a list of BackStackScreen.Items - return BackStackScreen(items: backStackItems) - } -} -``` - -![Welcome to Todo List](images/welcome-to-todolist.gif) - -Neat! We can now log in and log out, and show the name entered as our title! - -Next, we will add our Todo Editing screen. - -[Tutorial 3](Tutorial3.md) diff --git a/swift/Samples/Tutorial/Tutorial3.md b/swift/Samples/Tutorial/Tutorial3.md deleted file mode 100644 index 33f5284a9..000000000 --- a/swift/Samples/Tutorial/Tutorial3.md +++ /dev/null @@ -1,629 +0,0 @@ -# Step 3 - -_State throughout a tree of workflows_ - -## Setup - -To follow this tutorial: -- Open your terminal and run `bundle exec pod install` in the `swift/Samples/Tutorial` directory. -- Open `Tutorial.xcworkspace` and build the `Tutorial` Scheme. - -Start from implementation of `Tutorial2` if you're skipping ahead. You can run this by updating the `AppDelegate` to import `Tutorial2` instead of `TutorialBase`. - -## Editing TODO items - -Now that a user can "log in" to their TODO list, we want to add the ability to edit the TODO items listed. - -### State ownership - -In the workflow framework, data flows _down_ the tree as properties to child workflows, and actions come _up_ as output events (as in the traditional computer science trees that grow downward). - -What this means is that state should be created as far down the tree as possible, to limit the scope of state to be as small as possible. Additionally, there should be only one "owner" of the state in the tree - if it's passed farther down the tree, it should be a copy or read-only version of it - so there is no shared mutable state in multiple workflows. - -When a child workflow has a copy of the state from its parent, it should change it by emitting an _output_ back to the parent, requesting that it be changed. The child will then receive an updated snapshot of the data from the parent - keeping ownership at a single level of the tree. - -This is all a bit abstract, so let's make it more concrete by adding an edit todo item workflow. - -### Create an todo edit workflow and screen - -Using the templates, create a `TodoEditWorkflow` and `TodoEditScreen`. The `TodoEditSampleViewController` can now be deleted. - -#### TodoEditScreen - -Import the `TutorialViews` module and add the `TodoEditView` as a subview in the `TodoEditViewController` with the appropriate boilerplate to lay it out correctly. - -```swift -// Import the TutorialViews to use the pre-made view. -import TutorialViews -import Workflow -import WorkflowUI - - -struct TodoEditScreen: Screen { - // The `TodoEditScreen` is empty to start. We'll add the contents later on. -} - - -final class TodoEditViewController: ScreenViewController { - // The `todoEditView` has all the logic for displaying the todo and editing. - let todoEditView: TodoEditView - - required init(screen: TodoEditScreen) { - self.todoEditView = TodoEditView(frame: .zero) - - super.init(screen: screen) - update(with: screen) - } - - override func viewDidLoad() { - super.viewDidLoad() - - view.addSubview(todoEditView) - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - - todoEditView.frame = view.bounds.inset(by: view.safeAreaInsets) - } - - // ...rest of the implementation... -``` - -This view isn't particularly useful without the data to present it, so update the `TodoEditScreen` to add the properties we need and the callbacks: - -```swift -struct TodoEditScreen: Screen { - // The title of this todo item. - var title: String - // The contents, or "note" of the todo. - var note: String - - // Callback for when the title or note changes - var onTitleChanged: (String) -> Void - var onNoteChanged: (String) -> Void -} -``` - -The `Screen` protocol also requires a `viewControllerDescription` property. This describes the `UIViewController` that will be used to render the screen: - -```swift -extension TodoEditScreen { - func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { - TodoEditViewController.description(for: self, environment: environment) - } -} -``` - -Then update the view with the data from the screen: - -```swift -final class TodoEditViewController: ScreenViewController { - // ..rest of the implementation... - - override func screenDidChange(from previousScreen: TodoEditScreen) { - update(with: screen) - } - - private func update(with screen: TodoEditScreen) { - // Update the view with the data from the screen. - todoEditView.title = screen.title - todoEditView.note = screen.note - todoEditView.onTitleChanged = screen.onTitleChanged - todoEditView.onNoteChanged = screen.onNoteChanged - } - -} -``` - -#### TodoEditWorkflow - -Now that we have our screen and view controller, update the `TodoEditWorkflow` to emit this screen as the rendering. - -The `TodoEditWorkflow` needs an initial Todo item passed into it from its parent. It will make a copy of it in its internal state - this can be the "scratch pad" for edits. This allows changes to be made and still be able to discard the changes if the user does not want to save them. - -Additionally, we will (finally) use the `workflowDidChange` method. If the edit workflow's parent provides an updated `todo`, it will invalidate the `todo` in `State`, and replace it with the one provided from the parent. - -```swift -// MARK: Input and Output - -struct TodoEditWorkflow: Workflow { - - // The "Todo" passed from our parent. - var initialTodo: TodoModel - - enum Output { - - } -} - - -// MARK: State and Initialization - -extension TodoEditWorkflow { - - struct State { - // The workflow's copy of the Todo item. Changes are local to this workflow. - var todo: TodoModel - } - - func makeInitialState() -> TodoEditWorkflow.State { - return State(todo: initialTodo) - } - - func workflowDidChange(from previousWorkflow: TodoEditWorkflow, state: inout State) { - - // The `Todo` from our parent changed. Update our internal copy so we are starting from the same item. - // The "correct" behavior depends on the business logic - would we only want to update if the - // users hasn't changed the todo from the initial one? Or is it ok to delete whatever edits - // were in progress if the state from the parent changes? - if previousWorkflow.initialTodo != self.initialTodo { - state.todo = self.initialTodo - } - } -} -``` - -Next, define the actions this workflow will handle - specifically the title and note changing from the UI: - -```swift -// MARK: Actions - -extension TodoEditWorkflow { - - enum Action: WorkflowAction { - - typealias WorkflowType = TodoEditWorkflow - - case titleChanged(String) - case noteChanged(String) - - func apply(toState state: inout TodoEditWorkflow.State) -> TodoEditWorkflow.Output? { - - switch self { - - case .titleChanged(let title): - state.todo.title = title - - case .noteChanged(let note): - state.todo.note = note - } - - return nil - } - } -} -``` - -Finally, update the `render` method to return a `TodoEditScreen`: - -```swift -// MARK: Rendering - -extension TodoEditWorkflow { - - typealias Rendering = TodoEditScreen - - func render(state: TodoEditWorkflow.State, context: RenderContext) -> Rendering { - // The sink is used to send actions back to this workflow. - let sink = context.makeSink(of: Action.self) - - let todoEditScreen = TodoEditScreen( - title: state.todo.title, - note: state.todo.note, - onTitleChanged: { title in - sink.send(.titleChanged(title)) - }, - onNoteChanged: { note in - sink.send(.noteChanged(note)) - }) - - return todoEditScreen - } -} -``` - -Now the workflow provides a backing for the UI to edit a todo item, but doesn't support saving and discarding changes. Add two `Output`s and actions for these cases: - -```swift -// MARK: Input and Output - -struct TodoEditWorkflow: Workflow { - - // The "Todo" passed from our parent. - var initialTodo: TodoModel - - enum Output { - case discard - case save(TodoModel) - } -} - -// ..rest of implementation... - -// MARK: Actions - -extension TodoEditWorkflow { - - enum Action: WorkflowAction { - - typealias WorkflowType = TodoEditWorkflow - - case titleChanged(String) - case noteChanged(String) - case discardChanges - case saveChanges - - func apply(toState state: inout TodoEditWorkflow.State) -> TodoEditWorkflow.Output? { - - switch self { - - case .titleChanged(let title): - state.todo.title = title - - case .noteChanged(let note): - state.todo.note = note - - case .discardChanges: - // Return the .discard output when the discard action is received. - return .discard - - case .saveChanges: - // Return the .save output with the current todo state when the save action is received. - return .save(state.todo) - } - - return nil - } - } -} - -// ..rest of implementation... -``` - -We now need to have a way of getting the `save` and `discard` actions from the screen. We'll put the `TodoEditScreen` inside of a `BackStackScreen.Item`, so that there is a title bar with `save` and `back.Item` as our two buttons. - -```swift -// Don't forget to import `BackStackContainer` to be able to use `BackStackScreen`. -import BackStackContainer - -// MARK: Rendering - -extension TodoEditWorkflow { - - typealias Rendering = BackStackScreen.Item - - func render(state: TodoEditWorkflow.State, context: RenderContext) -> Rendering { - // The sink is used to send actions back to this workflow. - let sink = context.makeSink(of: Action.self) - - let todoEditScreen = TodoEditScreen( - title: state.todo.title, - note: state.todo.note, - onTitleChanged: { title in - sink.send(.titleChanged(title)) - }, - onNoteChanged: { note in - sink.send(.noteChanged(note)) - }) - - let backStackItem = BackStackScreen.Item( - key: "edit", - screen: todoEditScreen, - barContent: BackStackScreen.BarContent( - title: "Edit", - leftItem: .button(.back(handler: { - sink.send(.discardChanges) - })), - rightItem: .button(BackStackScreen.BarContent.Button( - content: .text("Save"), - handler: { - sink.send(.saveChanges) - })))) - return backStackItem - } -} -``` - -## Todo Editing in the full flow - -### Updating the current workflows to prepare to add the edit workflow - -We want the todo edit screen to be shown when a user taps on an item on the todo list screen. To do this, we will modify the todo list workflow to show the edit screen when we are editing. - -Because the `TodoEditWorkflow` returns a `BackStackScreen.Item`, we will first need to modify it to return a list of `BackStackScreen.Item`s as the rendering. - -```swift -// TodoListWorkflow - -import Workflow -import WorkflowUI -import BackStackContainer -import ReactiveSwift - - -// MARK: Input and Output - -struct TodoListWorkflow: Workflow { - - // The name is an input. - var name: String - - enum Output { - case back - } -} - -// ... - -// MARK: Actions - -extension TodoListWorkflow { - - enum Action: WorkflowAction { - - typealias WorkflowType = TodoListWorkflow - - case onBack - - func apply(toState state: inout TodoListWorkflow.State) -> TodoListWorkflow.Output? { - - switch self { - - case .onBack: - // When a `.onBack` action is received, emit a `.back` output - return .back - } - - } - } -} - -// ... - -// MARK: Rendering - -extension TodoListWorkflow { - - typealias Rendering = [BackStackScreen.Item] - - func render(state: TodoListWorkflow.State, context: RenderContext) -> Rendering { - - // Define a sink to be able to send the .onBack action. - let sink = context.makeSink(of: Action.self) - - let titles = state.todos.map { (todoModel) -> String in - return todoModel.title - } - let todoListScreen = TodoListScreen( - todoTitles: titles, - onTodoSelected: { _ in }) - - let backStackItem = BackStackScreen.Item( - key: "list", - screen: todoListScreen, - barContent: BackStackScreen.BarContent( - title: "Welcome \(name)", - leftItem: .button(.back(handler: { - // When the left button is tapped, send the .onBack action. - sink.send(.onBack) - })), - rightItem: .none)) - - return [backStackItem] - } -} -``` - -Next, update the `RootWorkflow` to pass the name into the `TodoListWorkflow` and handle the `.back` output: - -```swift -// MARK: Rendering - -extension RootWorkflow { - - typealias Rendering = BackStackScreen - - func render(state: RootWorkflow.State, context: RenderContext) -> Rendering { - // Delete the `let sink = context.makeSink(of: ...) as we no longer need a sink. - - // ... rest of the implementation of `render` - - switch state { - // When the state is `.welcome`, defer to the WelcomeWorkflow. - case .welcome: - // We always add the welcome screen to the backstack, so this is a no op. - break - - // When the state is `.todo`, defer to the TodoListWorkflow. - case .todo(name: let name): - - let todoBackStackItems = TodoListWorkflow(name: name) - .mapOutput({ output -> Action in - switch output { - case .back: - // When receiving a `.back` output, treat it as a `.logout` action. - return .logout - } - }) - .rendered(with: context) - - // Add the todoBackStackItems to our BackStackItems. - backStackItems.append(contentsOf: todoBackStackItems) - } - - // Finally, return the BackStackScreen with a list of BackStackScreen.Items - return BackStackScreen(items: backStackItems) - } -} -``` - -Run the app again to validate it still behaves the same. - -### Adding the edit workflow as a child to the `TodoListWorkflow` - -Now that the `TodoListWorkflow`'s rendering is a list of BackStackScreen.Items, it can be updated to show the edit workflow when a `Todo` item is tapped. - -Modify the state to represent if the list is being viewed, or an item is being edited: - -```swift -// MARK: State and Initialization - -extension TodoListWorkflow { - - struct State { - var todos: [TodoModel] - var step: Step - enum Step { - // Showing the list of todo items. - case list - // Editing a single item. The state holds the index so it can be updated when a save action is received. - case edit(index: Int) - } - } - - func makeInitialState() -> TodoListWorkflow.State { - return State( - todos: [TodoModel( - title: "Take the cat for a walk", - note: "Cats really need their outside sunshine time. Don't forget to walk Charlie. Hamilton is less excited about the prospect.") - ], - step: .list) - } - - func workflowDidChange(from previousWorkflow: TodoListWorkflow, state: inout State) { - } - -} -``` - -Add actions for selecting a todo item as well as saving or discarding the changes: - -```swift -// MARK: Actions - -extension TodoListWorkflow { - - enum Action: WorkflowAction { - - typealias WorkflowType = TodoListWorkflow - - case onBack - case selectTodo(index: Int) - case discardChanges - case saveChanges(todo: TodoModel, index: Int) - - func apply(toState state: inout TodoListWorkflow.State) -> TodoListWorkflow.Output? { - - switch self { - - case .onBack: - // When a `.onBack` action is received, emit a `.back` output - return .back - - case .selectTodo(index: let index): - // When a todo item is selected, edit it. - state.step = .edit(index: index) - return nil - - case .discardChanges: - // When a discard action is received, return to the list. - state.step = .list - return nil - - case .saveChanges(todo: let todo, index: let index): - // When changes are saved, update the state of that `todo` item and return to the list. - state.todos[index] = todo - - state.step = .list - return nil - } - - } - } -} -``` - -Update the `render` method to defer to the `TodoEditWorkflow` when editing: - -```swift -// MARK: Rendering - -extension TodoListWorkflow { - - typealias Rendering = [BackStackScreen.Item] - - func render(state: TodoListWorkflow.State, context: RenderContext) -> Rendering { - - // Define a sink to be able to send actions. - let sink = context.makeSink(of: Action.self) - - let titles = state.todos.map { (todoModel) -> String in - return todoModel.title - } - let todoListScreen = TodoListScreen( - todoTitles: titles, - onTodoSelected: { index in - // Send the `selectTodo` action when a todo is selected in the UI. - sink.send(.selectTodo(index: index)) - }) - - let todoListItem = BackStackScreen.Item( - key: "list", - screen: todoListScreen, - barContent: BackStackScreen.BarContent( - title: "Welcome \(name)", - leftItem: .back(handler: { - // When the left button is tapped, send the .onBack action. - sink.send(.onBack) - }), - rightItem: .none)) - - switch state.step { - case .list: - // On the "list" step, return just the list screen. - return [todoListItem] - - case .edit(index: let index): - // On the "edit" step, return both the list and edit screens. - let todoEditItem = TodoEditWorkflow( - initialTodo: state.todos[index]) - .mapOutput({ output -> Action in - switch output { - - case .discard: - // Send the discardChanges action when the discard output is received. - return .discardChanges - - case .save(let todo): - // Send the saveChanges action when the save output is received. - return .saveChanges(todo: todo, index: index) - } - }) - .rendered(with: context) - - return [todoListItem, todoEditItem] - } - } -} -``` - -Now we have a (nearly) fully formed app! Try it out and see how the data flows between the different workflows: - -![Edit-flow](images/full-edit-flow.gif) - -### Data Flow - -Looking at what was just built, this demonstrates how state should be handled in a tree of workflows. The `TodoListWorkflow` is responsible for the state of all the todo items. - -When an item is edited, the `TodoEditWorkflow` makes a _copy_ of it for its local state. The updates happen from the UI events (changing the title or note). Depending on if the user wants to save (hikes are fun!) or discard the changes (taking the cat for a swim is likely a bad idea), it emits an output of `discard` or `save`. - -When a `save` output is emitted, it includes the updated todo model. The parent (`TodoListWorkflow`) updates its internal state for that one item. The child never knows the index of the item being edited, it only has the minimum state of the specific item. This lets the parent be able to safely update its array of todos without being concerned about index-out-of-bounds errors. - -If so desired, the `TodoListWorkflow` could have additional checks for saving the changes. For instance, if the todo list was something fetched from a server, it may decide to discard any changes if the list was updated remotely, etc. - -## Up Next - -We have a pretty fully formed app. However if we want to keep going and adding features, we may want to reshape our tree of workflows. In the next tutorial, we'll cover refactoring and changing the shape of our workflow hierarchy. - -[Tutorial 4](Tutorial4.md) diff --git a/swift/Samples/Tutorial/Tutorial4.md b/swift/Samples/Tutorial/Tutorial4.md deleted file mode 100644 index ac683b2e6..000000000 --- a/swift/Samples/Tutorial/Tutorial4.md +++ /dev/null @@ -1,436 +0,0 @@ -# Step 4 - -_Refactoring and rebalancing a tree of Workflows_ - -## Setup - -To follow this tutorial: -- Open your terminal and run `bundle exec pod install` in the `swift/Samples/Tutorial` directory. -- Open `Tutorial.xcworkspace` and build the `Tutorial` Scheme. - -Start from implementation of `Tutorial3` if you're skipping ahead. You can run this by updating the `AppDelegate` to import `Tutorial3` instead of `TutorialBase`. - -## Adding new TODO items - -A gap in the usability of the TODO app is that it does not let the user create new todo items. We will add an "add" button on the right side of the navigation bar for this. - -## Refactoring a workflow by splitting it into a parent and child - -The `TodoListWorkflow` has started to grow, and has multiple concerns it's handling - specifically all of the `ListScreen` behavior, as well as the actions that can come from the `TodoEditWorkflow`. - -When a single workflow seems to be doing too many things, the common pattern is to extract some of its responsibilty into a parent. - -### TodoWorkflow - -Create a new workflow called `Todo` that will be responsible for both the `TodoListWorkflow` and the `TodoEditWorkflow`. - -```swift -import Workflow -import WorkflowUI -import BackStackContainer -import ReactiveSwift - - -// MARK: Input and Output - -struct TodoWorkflow: Workflow { - - enum Output { - - } -} -// ...rest of the template contents... -``` - -#### Moving logic from the TodoList to the TodoWorkflow - -Move the `todo` state, input, and outputs from the `TodoListWorkflow` up to the `TodoWorkflow`. It will be owner the list of todo items, and the `TodoListWorkflow` will simply show whatever is passed into its input: - -```swift -// TodoWorkflow.swift - -// MARK: Input and Output - -struct TodoWorkflow: Workflow { - var name: String - - enum Output { - case back - } -} - - -// MARK: State and Initialization - -extension TodoWorkflow { - - struct State { - var todos: [TodoModel] - var step: Step - enum Step { - // Showing the list of todo items. - case list - // Editing a single item. The state holds the index so it can be updated when a save action is received. - case edit(index: Int) - } - } - - func makeInitialState() -> TodoWorkflow.State { - return State( - todos: [TodoModel( - title: "Take the cat for a walk", - note: "Cats really need their outside sunshine time. Don't forget to walk Charlie. Hamilton is less excited about the prospect.") - ], - step: .list) - } - -// ...rest of the implementation... -``` - -Define the output events from the `TodoListWorkflow` to describe the `new` item action and selecting a todo item, as well as removing the todo list from the `State`: - -```swift -// TodoListWorkflow.swift -// MARK: Input and Output - -struct TodoListWorkflow: Workflow { - - // The name is an input. - var name: String - // Use the list of todo items passed from our parent. - var todos: [TodoModel] - - enum Output { - case back - case selectTodo(index: Int) - case newTodo - } -} - - -// MARK: State and Initialization - -extension TodoListWorkflow { - - struct State { - } - - func makeInitialState() -> TodoListWorkflow.State { - return State() - } - - func workflowDidChange(from previousWorkflow: TodoListWorkflow, state: inout State) { - - } -} -``` - -Change the `Action` behaviors to return an output instead of modifying any state: - -```swift -// MARK: Actions - -extension TodoListWorkflow { - - enum Action: WorkflowAction { - - typealias WorkflowType = TodoListWorkflow - - case onBack - case selectTodo(index: Int) - case new - - func apply(toState state: inout TodoListWorkflow.State) -> TodoListWorkflow.Output? { - - switch self { - - case .onBack: - // When a `.onBack` action is received, emit a `.back` output - return .back - - case .selectTodo(index: let index): - // Tell our parent that a todo item was selected. - return .selectTodo(index: index) - - case .new: - // Tell our parent a new todo item should be created. - return .newTodo - } - - } - } -} -``` - -Update the `render` method to only return the `TodoListScreen` as a `BackStackScreen.Item`, including the "new todo" button: - -```swift -// MARK: Rendering - -extension TodoListWorkflow { - - typealias Rendering = BackStackScreen.Item - - func render(state: TodoListWorkflow.State, context: RenderContext) -> Rendering { - - // Define a sink to be able to send the .onBack action. - let sink = context.makeSink(of: Action.self) - - let titles = todos.map { (todoModel) -> String in - return todoModel.title - } - let todoListScreen = TodoListScreen( - todoTitles: titles, - onTodoSelected: { index in - // Send the `selectTodo` action when a todo is selected in the UI. - sink.send(.selectTodo(index: index)) - }) - - let todoListItem = BackStackScreen.Item( - key: "list", - screen: todoListScreen, - barContent: BackStackScreen.BarContent( - title: "Welcome \(name)", - leftItem: .button(.back(handler: { - // When the left button is tapped, send the .onBack action. - sink.send(.onBack) - })), - rightItem: .button(BackStackScreen.BarContent.Button( - content: .text("New Todo"), - handler: { - sink.send(.new) - })))) - - return todoListItem - } -} -``` - -And add rendering the `TodoListWorkflow` and output handling in the `TodoWorkflow`: - -```swift -// MARK: Actions - -extension TodoWorkflow { - - enum Action: WorkflowAction { - - typealias WorkflowType = TodoWorkflow - - case back - case editTodo(index: Int) - case newTodo - - func apply(toState state: inout TodoWorkflow.State) -> TodoWorkflow.Output? { - - switch self { - case .back: - return .back - - case .editTodo(index: let index): - state.step = .edit(index: index) - - case .newTodo: - // Append a new todo model to the end of the list. - state.todos.append(TodoModel( - title: "New Todo", - note: "")) - } - - return nil - } - } -} - - -// MARK: Rendering - -extension TodoWorkflow { - - typealias Rendering = [BackStackScreen.Item] - - func render(state: TodoWorkflow.State, context: RenderContext) -> Rendering { - - let todoListItem = TodoListWorkflow( - name: name, - todos: state.todos) - .mapOutput({ output -> Action in - switch output { - - case .back: - return .back - - case .selectTodo(index: let index): - return .editTodo(index: index) - - case .newTodo: - return .newTodo - } - }) - .rendered(with: context) - - return [todoListItem] - - } -} -``` - -Updating the `RootWorkflow` to defer to the `TodoWorkflow` for rendering the `todo` state will get us back into a state where we can build again (albeit without editing support): - -```swift -// MARK: Rendering - -extension RootWorkflow { - - typealias Rendering = BackStackScreen - - func render(state: RootWorkflow.State, context: RenderContext) -> Rendering { - - // ... rest of the implementation ... - - switch state { - // When the state is `.welcome`, defer to the WelcomeWorkflow. - case .welcome: - // We always add the welcome screen to the backstack, so this is a no op. - break - - // When the state is `.todo`, defer to the TodoListWorkflow. - case .todo(name: let name): - - // was: let todoBackStackItems = TodoListWorkflow(name: name) - let todoBackStackItems = TodoWorkflow(name: name) - .mapOutput({ output -> Action in - switch output { - case .back: - // When receiving a `.back` output, treat it as a `.logout` action. - return .logout - } - }) - .rendered(with: context) - - backStackItems.append(contentsOf: todoBackStackItems) - } - - // Finally, return the BackStackScreen with a list of BackStackScreen.Items - return BackStackScreen(items: backStackItems) - } -} -``` - -#### Moving Edit Output handling to the TodoWorkflow - -The `TodoWorkflow` now can handle the outputs from the `TodoListWorkflow`, so next add handling the `TodoEditWorkflow` output events. - -Since the types of output and actions are pretty different from their origin, make a *second* action type on the `TodoWorkflow`: - -```swift -// MARK: Actions - -extension TodoWorkflow { - - // Was `enum Action: WorkflowAction {` - enum ListAction: WorkflowAction { - - // ... rest of List action definition and implementation ... - } - - - enum EditAction: WorkflowAction { - - typealias WorkflowType = TodoWorkflow - - case discardChanges - case saveChanges(index: Int, todo: TodoModel) - - func apply(toState state: inout TodoWorkflow.State) -> TodoWorkflow.Output? { - guard case .edit = state.step else { - fatalError("Received edit action when state was not `.edit`.") - } - - switch self { - - case .discardChanges: - state.step = .list - - case .saveChanges(index: let index, todo: let updatedTodo): - state.todos[index] = updatedTodo - - } - // Return to the list view for either a discard or save action. - state.step = .list - - return nil - } - } -} -``` - -And update the `render` method to show the `TodoEditWorkflow` screen when on the edit step: - -```swift -// MARK: Rendering - -extension TodoWorkflow { - - typealias Rendering = [BackStackScreen.Item] - - func render(state: TodoWorkflow.State, context: RenderContext) -> Rendering { - - let todoListItem = TodoListWorkflow( - name: name, - todos: state.todos) - .mapOutput({ output -> ListAction in - switch output { - - case .back: - return .back - - case .selectTodo(index: let index): - return .editTodo(index: index) - - case .newTodo: - return .newTodo - } - }) - .rendered(with: context) - - switch state.step { - - case .list: - // Return only the list item. - return [todoListItem] - - case .edit(index: let index): - - let todoEditItem = TodoEditWorkflow( - initialTodo: state.todos[index]) - .mapOutput({ output -> EditAction in - switch output { - case .discard: - return .discardChanges - - case .save(let updatedTodo): - return .saveChanges(index: index, todo: updatedTodo) - } - }) - .rendered(with: context) - - // Return both the list item and edit. - return [todoListItem, todoEditItem] - } - - } -} -``` - -That's it! There is now a workflow for both of our current steps of the Todo flow. We also used the ability to define multiple actions for a single workflow to keep the logic contained to the expected state we would receive the actions from. - -## Conclusion - -Is the code better after this refactor? It's debatable - having the logic in the `TodoListWorkflow` was probably ok for the scope of what the app is doing. - -However, if more screens are added to this flow it would be much easier to reason about, as there would be a single touchpoint controlling where we are within the subflow of viewing and editing todo items. - -Additionally, now the `TodoList` and `TodoEdit` workflows are completely decoupled - there is no longer a requirement that the `TodoEdit` workflow is displayed after the list. For instance, we could change the list to have "viewing" or "editing" modes, where tapping on an item might only allow it to be viewed, but another mode would allow editing. - -It comes down to the individual judgement of the developer to decide how a tree of workflows should be shaped - this was intended to provide two examples of how this _could_ be structured, but not specify how it _should_. diff --git a/swift/Samples/Tutorial/Tutorial5.md b/swift/Samples/Tutorial/Tutorial5.md deleted file mode 100644 index 0cea7673e..000000000 --- a/swift/Samples/Tutorial/Tutorial5.md +++ /dev/null @@ -1,843 +0,0 @@ -# Step 5 - -_Unit and Integration Testing Workflows_ - -## Setup - -To follow this tutorial: -- Open your terminal and run `bundle exec pod install` in the `swift/Samples/Tutorial` directory. -- Open `Tutorial.xcworkspace` and build the `Tutorial` Scheme. -- The unit tests will run from the default scheme when pressing `cmd+shift+u`. - -Start from implementation of `Tutorial4` if you're skipping ahead. You can run this by updating the `AppDelegate` to import `Tutorial4` instead of `TutorialBase`. - -# Testing - -`Workflow`s being easily testable was a design requirement. It is essential to building scalable, reliable software. - -The `WorkflowTesting` library is provided to allow easy unit and integration testing. - -## Unit Tests (Actions) - -A `WorkflowAction`'s `apply` function is effectively a reducer. Given a current state and action, it returns a new state (and optionally an output). Because `apply` function should almost always be a "pure" function, it is a great candidate for unit testing. - -The `WorkflowActionTester` is provided to facilitate writing unit tests against actions. - -## WorkflowActionTester - -The `WorkflowActionTester` is an extension on `WorkflowAction` which provides an easy to use harness for testing a series of actions and the resulting state updates. From the example in the source: -```swift -/// TestedWorklfow.Action -/// .tester(withState: .firstState) -/// .assertState { state in -/// XCTAssertEqual(.firstState, state) -/// } -/// .send(action: .exampleEvent) { output in -/// XCTAssertEqual(.finished, output) -/// } -/// .assertState { state in -/// XCTAssertEqual(.differentState, state) -/// } -``` - -It's provided with an initial state, and drives the state forward by sending one action at a time. The `Output` can be validated after each action is sent, as well as the `State`. - -### WelcomeWorkflow Tests - -Start by creating a new Unit test file called `WelcomeWorkflowTests`. Import `WorkflowTesting` as well as a `@testable import` for the `Tutorial` pod you're testing: - -We'll use the `@testable import` to be able to test the our workflows which are not exposed publicly. - -```swift -import XCTest -@testable import TutorialBase -import WorkflowTesting - - -class WelcomeWorkflowTests: XCTestCase { - - func testExample() { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct results. - } - -} -``` - -For the `WelcomeWorkflow`, we will start by testing that the `name` property is updated on the state every time a `.nameChanged` action is received: - -```swift -import XCTest -@testable import TutorialBase -import WorkflowTesting - - -class WelcomeWorkflowTests: XCTestCase { - - func testNameUpdates() { - WelcomeWorkflow.Action - .tester(withState: WelcomeWorkflow.State(name: "")) - .assertState { state in - // The initial state provided was an empty name. - XCTAssertEqual("", state.name) - } - .send(action: .nameChanged(name: "myName")) { output in - // No output is expected when the name changes. - XCTAssertNil(output) - } - .assertState { state in - // The `name` has been updated from the action. - XCTAssertEqual("myName", state.name) - } - } - -} -``` - -The `Output` of an action can also be tested. Next, we'll add a test for the `.didLogin` action. - -```swift - func testLogin() { - WelcomeWorkflow.Action - .tester(withState: WelcomeWorkflow.State(name: "")) - .send(action: .didLogin) { output in - switch output { - // When the `.didLogin` action is received, it should emit the `didLogin(name)` output. - case .didLogin(let name)?: - XCTAssertEqual("", name) - case nil: - XCTFail("Did not receive an output for .didLogin") - } - } - } -``` - -We have now validated that an output is emitted when the `.didLogin` action is received. However, while writing this test, it probably doesn't make sense to allow someone to log in without providing a name. Let's update the test to ensure that login is only allowed when there is a name: - -```swift - func testLogin() { - WelcomeWorkflow.Action - .tester(withState: WelcomeWorkflow.State(name: "")) - .send(action: .didLogin) { output in - // Since the name is empty, `.didLogin` will not emit an output. - XCTAssertNil(output) - } - .assertState { state in - // The name is empty, as was specified in the initial state. - XCTAssertEqual("", state.name) - } - .send(action: .nameChanged(name: "MyName")) { output in - // Update the name. - XCTAssertNil(output) - } - .assertState { state in - // Validate the name was updated. - XCTAssertEqual("MyName", state.name) - } - .send(action: .didLogin) { output in - // Now a `.didLogin` output should be emitted when the `.didLogin` action was received. - switch output { - case .didLogin(let name)?: - XCTAssertEqual("MyName", name) - case nil: - XCTFail("Did not receive an output for .didLogin") - } - } - } -``` - -The test will now fail, as a `.didLogin` action will still cause `.didLogin` output when the name is blank. Update the `WelcomeWorkflow` logic to reflect the new behavior we want: - -```swift -// MARK: Actions - -extension WelcomeWorkflow { - - enum Action: WorkflowAction { - - typealias WorkflowType = WelcomeWorkflow - - case nameChanged(name: String) - case didLogin - - func apply(toState state: inout WelcomeWorkflow.State) -> WelcomeWorkflow.Output? { - - switch self { - case .nameChanged(name: let name): - // Update our state with the updated name. - state.name = name - // Return `nil` for the output, we want to handle this action only at the level of this workflow. - return nil - - case .didLogin: - if state.name.count != 0 { - // Return an output of `didLogin` with the name if it's not empty. - return .didLogin(name: state.name) - } else { - // Don't log in if the name isn't filled in. - return nil - } - } - } - } -} -``` - -Run the test again and ensure that it passes. Additionally, try the app to see that it also reflects the updated behavior. - -### TodoListWorkflow - -Add tests for the `TodoListWorkflow`. They'll be pretty simple, as this workflow is stateless and all actions are simply forwarded to the parent as outputs: - -```swift -import XCTest -@testable import TutorialBase -import WorkflowTesting - - -class TodoListWorkflowTests: XCTestCase { - - func testActions() { - TodoListWorkflow - .Action - .tester(withState: TodoListWorkflow.State()) - .send(action: .onBack) { output in - // The `.onBack` action should emit an output of `.back`. - switch output { - case .back?: - break // Expected - default: - XCTFail("Expected an output of `.back`") - } - } - .send(action: .selectTodo(index: 7)) { output in - // The `.selectTodo` action should emit a `.selectTodo` output. - switch output { - case .selectTodo(let index)?: - XCTAssertEqual(7, index) - default: - XCTFail("Expected an output of `.selectTodo`") - } - } - .send(action: .new) { output in - // The`.new` action should emit a `.newTodo` output. - switch output { - case .newTodo?: - break // Expected - default: - XCTFail("Expected an output of `.newTodo`") - } - } - } - -} -``` - -### TodoEditWorkflow - -The `TodoEditWorkflow` has a bit more complexity since it holds a local copy of the todo to be edited. Start by adding tests for the actions: - -```swift -import XCTest -@testable import TutorialBase -import WorkflowTesting - - -class TodoEditWorkflowTests: XCTestCase { - - func testAction() { - TodoEditWorkflow - .Action - // Start with a todo of "Title" "Note" - .tester( - withState: TodoEditWorkflow.State( - todo: TodoModel(title: "Title", note: "Note"))) - .assertState { state in - XCTAssertEqual("Title", state.todo.title) - XCTAssertEqual("Note", state.todo.note) - } - // Update the title to "Updated Title" - .send(action: .titleChanged("Updated Title")) { output in - XCTAssertNil(output) - } - // Validate that only the title changed. - .assertState { state in - XCTAssertEqual("Updated Title", state.todo.title) - XCTAssertEqual("Note", state.todo.note) - } - // Update the note. - .send(action: .noteChanged("Updated Note")) { output in - XCTAssertNil(output) - } - // Validate that the note updated. - .assertState { state in - XCTAssertEqual("Updated Title", state.todo.title) - XCTAssertEqual("Updated Note", state.todo.note) - } - // Send a `.discardChanges` action, which will emit a `.discard` output. - .send(action: .discardChanges) { output in - switch output { - case .discard?: - break // Expected - default: - XCTFail("Expected an output of `.discard`") - } - } - // Send a `.saveChanges` action, which will emit a `.save` output with the updated todo model. - .send(action: .saveChanges) { output in - switch output { - case .save(let todo)?: - XCTAssertEqual("Updated Title", todo.title) - XCTAssertEqual("Updated Note", todo.note) - default: - XCTFail("Expected an output of `.save`") - } - } - } - -} -``` - -The `TodoEditWorkflow` also uses the `workflowDidChange` to update the internal state if its parent provides it with a different `todo`. Validate that this works as expected: - -```swift - func testChangedPropertyUpdatesLocalState() { - let initialWorkflow = TodoEditWorkflow(initialTodo: TodoModel(title: "Title", note: "Note")) - var state = initialWorkflow.makeInitialState() - // The initial state is a copy of the provided todo: - XCTAssertEqual("Title", state.todo.title) - XCTAssertEqual("Note", state.todo.note) - - // Mutate the internal state, simulating the change from actions: - state.todo.title = "Updated Title" - - // Update the workflow properties with the same value. The state should not be updated: - initialWorkflow.workflowDidChange(from: initialWorkflow, state: &state) - XCTAssertEqual("Updated Title", state.todo.title) - XCTAssertEqual("Note", state.todo.note) - - // The parent provided different properties. The internal state should be updated with the newly provided properties. - let updatedWorkflow = TodoEditWorkflow(initialTodo: TodoModel(title: "New Title", note: "New Note")) - updatedWorkflow.workflowDidChange(from: initialWorkflow, state: &state) - - XCTAssertEqual("New Title", state.todo.title) - XCTAssertEqual("New Note", state.todo.note) - } -``` - -## Testing Rendering - -Testing actions is very useful for validating all of the state transitions of a workflow, but it is beneficial to validate that the screens from `render` are expected. Since the `render` method uses a private implementation of a `RenderContext`, there is a `RenderTester` to facilitate testing. - -## RenderTester - -The `renderTester` extension on `Workflow` provides an easy way to test the rendering from a workflow. The simple usage of validating a rendering is shown in the doc comments: -```swift -workflow - .renderTester() - .render( - with: RenderExpectations(), - assertions: { rendering in - XCTAssertEqual("expected text on rendering", rendering.text) - } -``` - -It also provides a means to test that closures passed to screens cause the correct actions and state changes: - -```swift -workflow - .renderTester() - .render( - with: RenderExpectations( - expectedState: ExpectedState(state: TestWorkflow.State(text: "updated")), - assertions: { rendering in - XCTAssertEqual("expected text on rendering", rendering.text) - rendering.updateText("updated") - } -``` - -The full API allows for expected states, output, workers, and (child) workflows: -```swift -workflow - .renderTester(initialState: State()) - .render( - expectedState: ExpectedState(state: TestWorkflow.State(text: "updated")), - expectedOutput: ExpectedOutput(output: .completed), - expectedWorkers: [ExpectedWorker(worker: TestWorker(), output: .finished)], - expectedWorkflows: [ExpectedWorkflow(workflow: ChildWorkflow.self, rendering: ChildScreen(), output: .closed)], - assertions: { rendering in - XCTAssertEqual("expected text on rendering", rendering.text) - }) -``` - -### WelcomeWorkflow - -Add a test for the rendering of the `WelcomeWorkflow`: - -```swift -// WelcomeWorkflowTests.swift - - func testRendering() { - WelcomeWorkflow() - // Use the initial state provided by the welcome workflow - .renderTester() - .render(assertions: { screen in - XCTAssertEqual("", screen.name) - // Simulate tapping the login button. No output will be emitted, as the name is empty: - screen.onLoginTapped() - }) - // Next, simulate the name updating, expecting the state to be changed to reflect the updated name: - .render( - expectedState: ExpectedState( - state: WelcomeWorkflow.State(name: "myName"), - isEquivalent: { (expected, actual) -> Bool in - return expected.name == actual.name - }), assertions: { screen in - screen.onNameChanged("myName") - }) - // Finally, validate that `.didLogin` is sent when login is tapped with a non-empty name: - .render( - expectedOutput: ExpectedOutput(output: .didLogin(name: "myName"), isEquivalent: { (expected, actual) in - switch (expected, actual) { - case (.didLogin(name: let expectedName), .didLogin(name: let actualName)): - return expectedName == actualName - } - }), - assertions: { screen in - screen.onLoginTapped() - }) - } -``` - -Since the `State` and `Output` on the `WelcomeWorkflow` aren't equatable, we had to write our own equivalence method for them. To simplify this test, instead let's have both conform to `Equatable` to make the test a bit easier to read: - -```swift -// MARK: Input and Output - -struct WelcomeWorkflow: Workflow { - enum Output: Equatable { - case didLogin(name: String) - } -} - - -// MARK: State and Initialization - -extension WelcomeWorkflow { - - struct State: Equatable { - var name: String - } - -// ... rest of the implementation ... -``` - -Update the test to take advantage of the `Equatable` conformance: - - -```swift -func testRendering() { - WelcomeWorkflow() - // Use the initial state provided by the welcome workflow - .renderTester() - .render(assertions: { screen in - XCTAssertEqual("", screen.name) - // Simulate tapping the login button. No output will be emitted, as the name is empty: - screen.onLoginTapped() - }) - // Next, simulate the name updating, expecting the state to be changed to reflect the updated name: - .render( - expectedState: ExpectedState(state: WelcomeWorkflow.State(name: "myName")), - assertions: { screen in - screen.onNameChanged("myName") - }) - // Finally, validate that `.didLogin` is sent when login is tapped with a non-empty name: - .render( - expectedOutput: ExpectedOutput(output: .didLogin(name: "myName")), - assertions: { screen in - screen.onLoginTapped() - }) - } -``` - -Add tests against the `render` methods of the `TodoEdit` and `TodoList` workflows as desired. - -## Composition Testing - -We've demonstrated how to test leaf workflows for their actions and renderings. However, the power of workflow is the ability to compose a tree of workflows. The `RenderTester` provides the tools to test workflows with children. - -The `ExpectedWorkflow` allows a child workflow to be described that is expected for the next render. It is given the type of child, and optional key, and the mock rendering to return. It can also provide an optional output: -```swift -public struct ExpectedWorkflow { - - public init(type: WorkflowType.Type, key: String = "", rendering: WorkflowType.Rendering, output: WorkflowType.Output? = nil) - -} -``` - -### RootWorkflow Tests - -The `RootWorkflow` is responsible for the entire state of our app. We can skip testing the actions with the `ActionTester`, as that will be handled by testing the rendering. - -Start by adding `Equatable` conformance to the `State` to simplify the tests: - -```swift -extension RootWorkflow { - - // The state is an enum, and can either be on the welcome screen or the todo list. - // When on the todo list, it also includes the name provided on the welcome screen - enum State: Equatable { - // The welcome screen via the welcome workflow will be shown - case welcome - // The todo list screen via the todo list workflow will be shown. The name will be provided to the todo list. - case todo(name: String) - } -``` - -And first we can test the `.welcome` state on its own: - -```swift -import XCTest -@testable import TutorialBase -import WorkflowTesting -// Import `BackStackContainer` as testable so that the items in the `BackStackScreen` can be inspected. -@testable import BackStackContainer -// Import `WorkflowUI` as testable so that the wrappedScreen in `AnyScreen` can be accessed. -@testable import WorkflowUI - -class RootWorkflowTests: XCTestCase { - - func testWelcomeRendering() { - RootWorkflow() - // Start in the `.welcome` state - .renderTester(initialState: RootWorkflow.State.welcome) - .render( - // Expect the state to stay as `.welcome`. - expectedState: ExpectedState(state: RootWorkflow.State.welcome), - // No output is expected from the root workflow. - expectedOutput: nil, - // There are no workers that should be run. - expectedWorkers: [], - // The `WelcomeWorkflow` is expected to be started in this render. - expectedWorkflows: [ - ExpectedWorkflow( - type: WelcomeWorkflow.self, - // Simulate this as the `WelcomeScreen` returned by the `WelcomeWorkflow`. The callback can be stubbed out, as they won't be used. - rendering: WelcomeScreen( - name: "MyName", - onNameChanged: { _ in }, - onLoginTapped: {})) - ], - // Now, validate that there is a single item in the BackStackScreen, which is our welcome screen. - assertions: { rendering in - XCTAssertEqual(1, rendering.items.count) - guard let welcomeScreen = rendering.items[0].screen.wrappedScreen as? WelcomeScreen else { - XCTFail("Expected first screen to be a `WelcomeScreen`") - return - } - XCTAssertEqual("MyName", welcomeScreen.name) - }) - } - -} -``` - -We needed to use a few `@testable` imports to inspect the underlying screen (since both the `BackStackScreen` and `AnyScreen` use type-erasure), but we've been able to validate that the `RootWorkflow` renders as expected. - -Now, we can also test the transition from the `.welcome` state to the `.todo` state: - -```swift - func testLogin() { - RootWorkflow() - // Start in the `.welcome` state - .renderTester(initialState: RootWorkflow.State.welcome) - .render( - // Expect the state to transition to `.todo` - expectedState: ExpectedState(state: RootWorkflow.State.todo(name: "MyName")), - // No output is expected from the root workflow. - expectedOutput: nil, - // There are no workers that should be run. - expectedWorkers: [], - // The `WelcomeWorkflow` is expected to be started in this render. - expectedWorkflows: [ - ExpectedWorkflow( - type: WelcomeWorkflow.self, - // Simulate this as the `WelcomeScreen` returned by the `WelcomeWorkflow`. The callback can be stubbed out, as they won't be used. - rendering: WelcomeScreen( - name: "MyName", - onNameChanged: { _ in }, - onLoginTapped: {}), - // Simulate the `WelcomeWorkflow` sending an output of `.didLogin` as if the login button was tapped. - output: .didLogin(name: "MyName")) - ], - // Now, validate that there is a single item in the BackStackScreen, which is our welcome screen (prior to the output). - assertions: { rendering in - XCTAssertEqual(1, rendering.items.count) - guard let welcomeScreen = rendering.items[0].screen.wrappedScreen as? WelcomeScreen else { - XCTFail("Expected first screen to be a `WelcomeScreen`") - return - } - XCTAssertEqual("MyName", welcomeScreen.name) - }) - .assert(state: { state in - XCTAssertEqual(.todo(name: "MyName"), state) - }) - - } -``` - -By simulating the output from the `WelcomeWorkflow`, we were able to drive the `RootWorkflow` forward. This was much more of an integration test than a "pure" unit test, but we have now validated the same behavior we see by testing the app by hand. - -### TodoWorkflow Render Tests - -Now add tests for the `TodoWorkflow`, so that we have relatively full coverage. These are two examples, of selecting and saving a TODO to validate the transitions between screens, as well as updating the state in the parent (Add `Equatable` conformance to `TodoWorkflow.State` to simplify the tests): - -```swift -import XCTest -@testable import TutorialBase -import BackStackContainer -import WorkflowTesting - - -class TodoWorkflowTests: XCTestCase { - - func testSelectingTodo() { - let todos: [TodoModel] = [TodoModel(title: "Title", note: "Note")] - - TodoWorkflow(name: "MyName") - // Start from the list step to validate selecting a todo: - .renderTester(initialState: TodoWorkflow.State( - todos: todos, - step: .list)) - .render( - // Only specify the expected workflows for this render: - expectedWorkflows: [ - // We only expect the TodoListWorkflow - ExpectedWorkflow( - type: TodoListWorkflow.self, - rendering: BackStackScreen.Item( - screen: TodoListScreen( - todoTitles: ["Title"], - onTodoSelected: { _ in })), - // Simulate selecting the first todo: - output: TodoListWorkflow.Output.selectTodo(index: 0)), - ], - assertions: { items in - // Just validate that there is one item in the backstack. - // Additional validation could be done on the screens returned if so desired. - XCTAssertEqual(1, items.count) - }) - // Validate that the state was updated after the last render pass with the output from the TodoEditWorkflow. - .assert { state in - XCTAssertEqual( - TodoWorkflow.State( - todos: [TodoModel(title: "Title", note: "Note")], - step: .edit(index: 0)), - state) - } - } - - func testSavingTodo() { - let todos: [TodoModel] = [TodoModel(title: "Title", note: "Note")] - - TodoWorkflow(name: "MyName") - // Start from the edit step so we can simulate saving: - .renderTester(initialState: TodoWorkflow.State( - todos: todos, - step: .edit(index: 0))) - .render( - // Only specify the expected workflows for this render: - expectedWorkflows: [ - // We always expect the TodoListWorkflow - ExpectedWorkflow( - type: TodoListWorkflow.self, - rendering: BackStackScreen.Item( - screen: TodoListScreen( - todoTitles: ["Title"], - onTodoSelected: { _ in }))), - // Expect the TodoEditWorkflow. Additionally, simulate it emitting an output of ".save" to update the state. - ExpectedWorkflow( - type: TodoEditWorkflow.self, - rendering: BackStackScreen.Item(screen: TodoEditScreen( - title: "Title", - note: "Note", - onTitleChanged: { _ in }, - onNoteChanged: { _ in })), - output: TodoEditWorkflow.Output.save(TodoModel( - title: "Updated Title", - note: "Updated Note"))) - ], - assertions: { items in - // Just validate that there are two items in the backstack. - // Additional validation could be done on the screens returned if so desired. - XCTAssertEqual(2, items.count) - }) - // Validate that the state was updated after the last render pass with the output from the TodoEditWorkflow. - .assert { state in - XCTAssertEqual( - TodoWorkflow.State( - todos: [TodoModel(title: "Updated Title", note: "Updated Note")], - step: .list), - state) - } - } - -} -``` - -## Integration Testing - -The `RenderTester` allows easy "mocking" of child workflows and workers. However, this means that we are not exercising the full infrastructure (even though we could get a fairly high confidence from the tests). Sometimes, it may be worth putting together integration tests that test a full tree of Workflows. - -Add another test to `RootWorkflowTests`. We will run the tree of workflows in a `WorkflowHost`, which is what the infrastructure uses for a `ContainerViewController`. This will be a "black box" test, as we can only test the behaviors from the rendering and will not be able to inspect the underlying states. This may be a useful test for validation when refactoring a tree of workflows to ensure they behave the same way. - -```swift -// RootWorkflowTests.swift - func testAppFlow() { - let workflowHost = WorkflowHost(workflow: RootWorkflow()) - - // First rendering is just the welcome screen. Update the name. - do { - let backStack = workflowHost.rendering.value - XCTAssertEqual(1, backStack.items.count) - - guard let welcomeScreen = backStack.items[0].screen.wrappedScreen as? WelcomeScreen else { - XCTFail("Expected initial screen of `WelcomeScreen`") - return - } - - welcomeScreen.onNameChanged("MyName") - } - - // Log in and go to the welcome list - do { - let backStack = workflowHost.rendering.value - XCTAssertEqual(1, backStack.items.count) - - guard let welcomeScreen = backStack.items[0].screen.wrappedScreen as? WelcomeScreen else { - XCTFail("Expected initial screen of `WelcomeScreen`") - return - } - - welcomeScreen.onLoginTapped() - } - - // Expect the todo list. Edit the first todo. - do { - let backStack = workflowHost.rendering.value - XCTAssertEqual(2, backStack.items.count) - - guard let _ = backStack.items[0].screen.wrappedScreen as? WelcomeScreen else { - XCTFail("Expected first screen of `WelcomeScreen`") - return - } - - guard let todoScreen = backStack.items[1].screen.wrappedScreen as? TodoListScreen else { - XCTFail("Expected second screen of `TodoListScreen`") - return - } - XCTAssertEqual(1, todoScreen.todoTitles.count) - // Select the first todo: - todoScreen.onTodoSelected(0) - } - - // Selected a todo to edit. Expect the todo edit screen. - do { - let backStack = workflowHost.rendering.value - XCTAssertEqual(3, backStack.items.count) - - guard let _ = backStack.items[0].screen.wrappedScreen as? WelcomeScreen else { - XCTFail("Expected first screen of `WelcomeScreen`") - return - } - - guard let _ = backStack.items[1].screen.wrappedScreen as? TodoListScreen else { - XCTFail("Expected second screen of `TodoListScreen`") - return - } - - guard let editScreen = backStack.items[2].screen.wrappedScreen as? TodoEditScreen else { - XCTFail("Expected second screen of `TodoEditScreen`") - return - } - - // Update the title: - editScreen.onTitleChanged("New Title") - } - - // Save the selected todo. - do { - let backStack = workflowHost.rendering.value - XCTAssertEqual(3, backStack.items.count) - - guard let _ = backStack.items[0].screen.wrappedScreen as? WelcomeScreen else { - XCTFail("Expected first screen of `WelcomeScreen`") - return - } - - guard let _ = backStack.items[1].screen.wrappedScreen as? TodoListScreen else { - XCTFail("Expected second screen of `TodoListScreen`") - return - } - - guard let _ = backStack.items[2].screen.wrappedScreen as? TodoEditScreen else { - XCTFail("Expected second screen of `TodoEditScreen`") - return - } - - // Save the changes by tapping the right bar button. - // This also validates that the navigation bar was described as expected. - switch backStack.items[2].barVisibility { - - case .hidden: - XCTFail("Expected a visible navigation bar") - - case .visible(let barContent): - switch barContent.rightItem { - - case .none: - XCTFail("Expected a right bar button") - - case .button(let button): - - switch button.content { - - case .text(let text): - XCTAssertEqual("Save", text) - - case .icon: - XCTFail("Expected the right bar button to have a title of `Save`") - } - // Tap the right bar button to save. - button.handler() - } - } - } - - // Expect the todo list. Validate the title was updated. - do { - let backStack = workflowHost.rendering.value - XCTAssertEqual(2, backStack.items.count) - - guard let _ = backStack.items[0].screen.wrappedScreen as? WelcomeScreen else { - XCTFail("Expected first screen of `WelcomeScreen`") - return - } - - guard let todoScreen = backStack.items[1].screen.wrappedScreen as? TodoListScreen else { - XCTFail("Expected second screen of `TodoListScreen`") - return - } - XCTAssertEqual(1, todoScreen.todoTitles.count) - XCTAssertEqual("New Title", todoScreen.todoTitles[0]) - - } - - } -``` - -This test was *very* verbose, and rather long. Generally, it's not recommended to do full integration tests like this (the action tests and render tests can give pretty solid coverage of a workflow's behavior). However, this is an example of how it might be done in the case that it's needed. - -# Conclusion - -This was intended as a guide of how testing can be facilitated with the `WorkflowTesting` library provided for workflows. As always, it is up to the judgement of the developer of what and how their software should be tested. diff --git a/swift/Samples/Tutorial/images/empty-todolist.png b/swift/Samples/Tutorial/images/empty-todolist.png deleted file mode 100644 index 957d4883c..000000000 Binary files a/swift/Samples/Tutorial/images/empty-todolist.png and /dev/null differ diff --git a/swift/Samples/Tutorial/images/full-edit-flow.gif b/swift/Samples/Tutorial/images/full-edit-flow.gif deleted file mode 100644 index dde53e0f6..000000000 Binary files a/swift/Samples/Tutorial/images/full-edit-flow.gif and /dev/null differ diff --git a/swift/Samples/Tutorial/images/missing-map-output.png b/swift/Samples/Tutorial/images/missing-map-output.png deleted file mode 100644 index 41d700593..000000000 Binary files a/swift/Samples/Tutorial/images/missing-map-output.png and /dev/null differ diff --git a/swift/Samples/Tutorial/images/new-screen-todolist.png b/swift/Samples/Tutorial/images/new-screen-todolist.png deleted file mode 100644 index 5eac887f9..000000000 Binary files a/swift/Samples/Tutorial/images/new-screen-todolist.png and /dev/null differ diff --git a/swift/Samples/Tutorial/images/new-screen.png b/swift/Samples/Tutorial/images/new-screen.png deleted file mode 100644 index ecb754cbe..000000000 Binary files a/swift/Samples/Tutorial/images/new-screen.png and /dev/null differ diff --git a/swift/Samples/Tutorial/images/new-todolist-workflow.png b/swift/Samples/Tutorial/images/new-todolist-workflow.png deleted file mode 100644 index 50be6220e..000000000 Binary files a/swift/Samples/Tutorial/images/new-todolist-workflow.png and /dev/null differ diff --git a/swift/Samples/Tutorial/images/new-workflow.png b/swift/Samples/Tutorial/images/new-workflow.png deleted file mode 100644 index debf67347..000000000 Binary files a/swift/Samples/Tutorial/images/new-workflow.png and /dev/null differ diff --git a/swift/Samples/Tutorial/images/tut2-todolist-example.png b/swift/Samples/Tutorial/images/tut2-todolist-example.png deleted file mode 100644 index 1d2c7f2b1..000000000 Binary files a/swift/Samples/Tutorial/images/tut2-todolist-example.png and /dev/null differ diff --git a/swift/Samples/Tutorial/images/welcome-to-todolist.gif b/swift/Samples/Tutorial/images/welcome-to-todolist.gif deleted file mode 100644 index a617427e8..000000000 Binary files a/swift/Samples/Tutorial/images/welcome-to-todolist.gif and /dev/null differ diff --git a/swift/Samples/Tutorial/images/welcome.png b/swift/Samples/Tutorial/images/welcome.png deleted file mode 100644 index 5011ffeb1..000000000 Binary files a/swift/Samples/Tutorial/images/welcome.png and /dev/null differ diff --git a/swift/Samples/Tutorial/images/workflow-file-location.png b/swift/Samples/Tutorial/images/workflow-file-location.png deleted file mode 100644 index d1402fb0e..000000000 Binary files a/swift/Samples/Tutorial/images/workflow-file-location.png and /dev/null differ diff --git a/swift/Samples/Tutorial/images/workflow-name.png b/swift/Samples/Tutorial/images/workflow-name.png deleted file mode 100644 index 04200ddc6..000000000 Binary files a/swift/Samples/Tutorial/images/workflow-name.png and /dev/null differ diff --git a/swift/Tooling/Templates/Screen (View Controller).xctemplate/TemplateIcon.png b/swift/Tooling/Templates/Screen (View Controller).xctemplate/TemplateIcon.png deleted file mode 100644 index 09777a0fe..000000000 Binary files a/swift/Tooling/Templates/Screen (View Controller).xctemplate/TemplateIcon.png and /dev/null differ diff --git a/swift/Tooling/Templates/Screen (View Controller).xctemplate/TemplateIcon@2x.png b/swift/Tooling/Templates/Screen (View Controller).xctemplate/TemplateIcon@2x.png deleted file mode 100644 index 5237fec4c..000000000 Binary files a/swift/Tooling/Templates/Screen (View Controller).xctemplate/TemplateIcon@2x.png and /dev/null differ diff --git a/swift/Tooling/Templates/Screen (View Controller).xctemplate/TemplateInfo.plist b/swift/Tooling/Templates/Screen (View Controller).xctemplate/TemplateInfo.plist deleted file mode 100755 index a2c18d043..000000000 --- a/swift/Tooling/Templates/Screen (View Controller).xctemplate/TemplateInfo.plist +++ /dev/null @@ -1,29 +0,0 @@ - - - - - Kind - Xcode.IDEKit.TextSubstitutionFileTemplateKind - Platforms - - com.apple.platform.iphoneos - - Options - - - Identifier - productName - Required - - Name - Screen Name: - Description - The name of the screen to create - Type - text - Default - HelloWorld - - - - diff --git a/swift/Tooling/Templates/Screen (View Controller).xctemplate/___FILEBASENAME___Screen.swift b/swift/Tooling/Templates/Screen (View Controller).xctemplate/___FILEBASENAME___Screen.swift deleted file mode 100755 index f04841c8a..000000000 --- a/swift/Tooling/Templates/Screen (View Controller).xctemplate/___FILEBASENAME___Screen.swift +++ /dev/null @@ -1,30 +0,0 @@ -// ___FILEHEADER___ - -import Workflow -import WorkflowUI - -struct ___VARIABLE_productName___Screen: Screen { - // This should contain all data to display in the UI - - // It should also contain callbacks for any UI events, for example: - // var onButtonTapped: () -> Void - - func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { - return ___VARIABLE_productName___ViewController.description(for: self, environment: environment) - } -} - -final class ___VARIABLE_productName___ViewController: ScreenViewController<___VARIABLE_productName___Screen> { - required init(screen: ___VARIABLE_productName___Screen, environment: ViewEnvironment) { - super.init(screen: screen, environment: environment) - update(with: screen, environment: environment) - } - - override func screenDidChange(from previousScreen: ___VARIABLE_productName___Screen, previousEnvironment: ViewEnvironment) { - update(with: screen, environment: environment) - } - - private func update(with screen: ___VARIABLE_productName___Screen, environment: ViewEnvironment) { - /// Update UI - } -} diff --git a/swift/Tooling/Templates/Workflow (Verbose).xctemplate/TemplateIcon.png b/swift/Tooling/Templates/Workflow (Verbose).xctemplate/TemplateIcon.png deleted file mode 100644 index 8f765c333..000000000 Binary files a/swift/Tooling/Templates/Workflow (Verbose).xctemplate/TemplateIcon.png and /dev/null differ diff --git a/swift/Tooling/Templates/Workflow (Verbose).xctemplate/TemplateIcon@2x.png b/swift/Tooling/Templates/Workflow (Verbose).xctemplate/TemplateIcon@2x.png deleted file mode 100644 index fc140d065..000000000 Binary files a/swift/Tooling/Templates/Workflow (Verbose).xctemplate/TemplateIcon@2x.png and /dev/null differ diff --git a/swift/Tooling/Templates/Workflow (Verbose).xctemplate/TemplateInfo.plist b/swift/Tooling/Templates/Workflow (Verbose).xctemplate/TemplateInfo.plist deleted file mode 100644 index 25f79240a..000000000 --- a/swift/Tooling/Templates/Workflow (Verbose).xctemplate/TemplateInfo.plist +++ /dev/null @@ -1,29 +0,0 @@ - - - - - Kind - Xcode.IDEKit.TextSubstitutionFileTemplateKind - Platforms - - com.apple.platform.iphoneos - - Options - - - Identifier - productName - Required - - Name - Name: - Description - The name of the workflow to create. "Workflow" will be appended to the name. - Type - text - Default - HelloWorld - - - - diff --git a/swift/Tooling/Templates/Workflow (Verbose).xctemplate/___FILEBASENAME___Workflow.swift b/swift/Tooling/Templates/Workflow (Verbose).xctemplate/___FILEBASENAME___Workflow.swift deleted file mode 100644 index 96ea5004c..000000000 --- a/swift/Tooling/Templates/Workflow (Verbose).xctemplate/___FILEBASENAME___Workflow.swift +++ /dev/null @@ -1,62 +0,0 @@ -// ___FILEHEADER___ - -import ReactiveSwift -import Workflow -import WorkflowUI - -// MARK: Input and Output - -struct ___VARIABLE_productName___Workflow: Workflow { - enum Output {} -} - -// MARK: State and Initialization - -extension ___VARIABLE_productName___Workflow { - struct State {} - - func makeInitialState() -> ___VARIABLE_productName___Workflow.State { - return State() - } - - func workflowDidChange(from previousWorkflow: ___VARIABLE_productName___Workflow, state: inout State) {} -} - -// MARK: Actions - -extension ___VARIABLE_productName___Workflow { - enum Action: WorkflowAction { - typealias WorkflowType = ___VARIABLE_productName___Workflow - - func apply(toState state: inout ___VARIABLE_productName___Workflow.State) -> ___VARIABLE_productName___Workflow.Output? { - switch self { - // Update state and produce an optional output based on which action was received. - } - } - } -} - -// MARK: Workers - -extension ___VARIABLE_productName___Workflow { - struct ___VARIABLE_productName___Worker: Worker { - enum Output {} - - func run() -> SignalProducer { - fatalError() - } - - func isEquivalent(to otherWorker: ___VARIABLE_productName___Worker) -> Bool { - return true - } - } -} - -// MARK: Rendering - -extension ___VARIABLE_productName___Workflow { - func render(state: ___VARIABLE_productName___Workflow.State, context: RenderContext<___VARIABLE_productName___Workflow>) -> String { - #warning("Don't forget your compose implementation and to return the correct rendering type!") - return "This is likely not the rendering that you want to return" - } -} diff --git a/swift/Tooling/Templates/install-xcode-templates.sh b/swift/Tooling/Templates/install-xcode-templates.sh deleted file mode 100755 index 52982d06e..000000000 --- a/swift/Tooling/Templates/install-xcode-templates.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env sh - -# Configuration -XCODE_TEMPLATE_DIR=$HOME'/Library/Developer/Xcode/Templates/File Templates/Workflow' -SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" - -# Copy workflow file templates into the local workflow template directory -xcodeTemplate () { - echo "Copying workflow Xcode file templates..." - - mkdir -p "$XCODE_TEMPLATE_DIR" - - cp -R $SCRIPT_DIR/*.xctemplate "$XCODE_TEMPLATE_DIR" -} - -xcodeTemplate - -echo "Success!" -echo "Workflow templates have been installed. Remember to restart Xcode!" diff --git a/swift/Workflow/Sources/AnyWorkflow.swift b/swift/Workflow/Sources/AnyWorkflow.swift deleted file mode 100644 index 00ee08dc2..000000000 --- a/swift/Workflow/Sources/AnyWorkflow.swift +++ /dev/null @@ -1,156 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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. - */ - -/// A type-erased wrapper that contains a workflow with the given Rendering and Output types. -public struct AnyWorkflow { - private let storage: AnyStorage - - private init(storage: AnyStorage) { - self.storage = storage - } - - /// Initializes a new type-erased wrapper for the given workflow. - public init(_ workflow: T) where T.Rendering == Rendering, T.Output == Output { - self.init(storage: Storage( - workflow: workflow, - renderingTransform: { $0 }, - outputTransform: { $0 } - )) - } - - /// The underlying workflow's implementation type. - public var workflowType: Any.Type { - return storage.workflowType - } -} - -extension AnyWorkflow: AnyWorkflowConvertible { - public func asAnyWorkflow() -> AnyWorkflow { - return self - } -} - -extension AnyWorkflow { - /// Returns a new AnyWorkflow whose `Output` type has been transformed into the given type. - /// - /// - Parameter transform: An escaping closure that maps the original output type into the new output type. - /// - /// - Returns: A type erased workflow with the new output type (the rendering type remains unchanged). - public func mapOutput(_ transform: @escaping (Output) -> NewOutput) -> AnyWorkflow { - let storage = self.storage.mapOutput(transform: transform) - return AnyWorkflow(storage: storage) - } - - /// Returns a new `AnyWorkflow` whose `Rendering` type has been transformed into the given type. - /// - /// - Parameter transform: An escaping closure that maps the original rendering type into the new rendering type. - /// - /// - Returns: A type erased workflow with the new rendering type (the output type remains unchanged). - public func mapRendering(_ transform: @escaping (Rendering) -> NewRendering) -> AnyWorkflow { - let storage = self.storage.mapRendering(transform: transform) - return AnyWorkflow(storage: storage) - } - - /// Renders the underlying workflow implementation with the given context. - /// - /// We must invert the model here (by passing the context into the type, instead - /// of passing the type into the context) because the type signature of the - /// type-erased wrapper does not contain the underlying workflow's - /// implementation type. - /// - /// That type information *is* present in our storage object, however, so we - /// pass the context down to that storage object which will ultimately call - /// through to `context.render(workflow:key:reducer:)`. - internal func render(context: RenderContext, key: String, outputMap: @escaping (Output) -> AnyWorkflowAction) -> Rendering { - return storage.render(context: context, key: key, outputMap: outputMap) - } -} - -extension AnyWorkflow { - /// This is the type erased outer API (referenced by the containing AnyWorkflow). - /// - /// This type is never used directly. - fileprivate class AnyStorage { - func render(context: RenderContext, key: String, outputMap: @escaping (Output) -> AnyWorkflowAction) -> Rendering { - fatalError() - } - - func mapRendering(transform: @escaping (Rendering) -> NewRendering) -> AnyWorkflow.AnyStorage { - fatalError() - } - - func mapOutput(transform: @escaping (Output) -> NewOutput) -> AnyWorkflow.AnyStorage { - fatalError() - } - - var workflowType: Any.Type { - fatalError() - } - } - - /// Subclass that adds type information about the underlying workflow implementation. - /// - /// This is the only type that is ever actually used by AnyWorkflow as storage. - fileprivate final class Storage: AnyStorage { - let workflow: T - let renderingTransform: (T.Rendering) -> Rendering - let outputTransform: (T.Output) -> Output - - init(workflow: T, renderingTransform: @escaping (T.Rendering) -> Rendering, outputTransform: @escaping (T.Output) -> Output) { - self.workflow = workflow - self.renderingTransform = renderingTransform - self.outputTransform = outputTransform - } - - override var workflowType: Any.Type { - return T.self - } - - override func render(context: RenderContext, key: String, outputMap: @escaping (Output) -> AnyWorkflowAction) -> Rendering { - let outputMap: (T.Output) -> AnyWorkflowAction = { [outputTransform] output in - outputMap(outputTransform(output)) - } - let rendering = context.render(workflow: workflow, key: key, outputMap: outputMap) - return renderingTransform(rendering) - } - - override func mapOutput(transform: @escaping (Output) -> NewOutput) -> AnyWorkflow.AnyStorage { - return AnyWorkflow.Storage( - workflow: workflow, - renderingTransform: renderingTransform, - outputTransform: { transform(self.outputTransform($0)) } - ) - } - - override func mapRendering(transform: @escaping (Rendering) -> NewRendering) -> AnyWorkflow.AnyStorage { - return AnyWorkflow.Storage( - workflow: workflow, - renderingTransform: { transform(self.renderingTransform($0)) }, - outputTransform: outputTransform - ) - } - } -} - -extension AnyWorkflowConvertible { - public func mapOutput(_ transform: @escaping (Output) -> NewOutput) -> AnyWorkflow { - return asAnyWorkflow().mapOutput(transform) - } - - public func mapRendering(_ transform: @escaping (Rendering) -> NewRendering) -> AnyWorkflow { - return asAnyWorkflow().mapRendering(transform) - } -} diff --git a/swift/Workflow/Sources/AnyWorkflowConvertible.swift b/swift/Workflow/Sources/AnyWorkflowConvertible.swift deleted file mode 100644 index e882c3fff..000000000 --- a/swift/Workflow/Sources/AnyWorkflowConvertible.swift +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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. - */ - -/// Conforming types can be converted into `AnyWorkflow` values, allowing them to participate -/// in a workflow hierarchy. -public protocol AnyWorkflowConvertible { - /// The rendering type of this type's `AnyWorkflow` representation - associatedtype Rendering - - /// The output type of this type's `AnyWorkflow` representation - associatedtype Output - - /// Returns an `AnyWorkflow` representing this value. - func asAnyWorkflow() -> AnyWorkflow -} - -extension AnyWorkflowConvertible { - /// Creates or updates a child workflow of the given type, performs a render pass, and returns the result. - /// - /// Note that it is a programmer error to render two instances of a given workflow type with the same `key` - /// during the same render pass. - /// - /// - Parameter context: The context with which the workflow will be rendered. - /// - Parameter key: A string that uniquely identifies this workflow. - /// - /// - Returns: The `Rendering` generated by the workflow. - public func rendered(with context: RenderContext, key: String = "") -> Rendering where Output: WorkflowAction, Output.WorkflowType == Parent { - return asAnyWorkflow().render(context: context, key: key, outputMap: { AnyWorkflowAction($0) }) - } - - public func rendered(with context: RenderContext, key: String = "") -> Rendering where Output == AnyWorkflowAction { - return asAnyWorkflow().render(context: context, key: key, outputMap: { $0 }) - } -} - -extension AnyWorkflowConvertible where Output == Never { - /// Creates or updates a child workflow of the given type, performs a render pass, and returns the result. - /// - /// Note that it is a programmer error to render two instances of a given workflow type with the same `key` - /// during the same render pass. - /// - /// - Parameter context: The context with which the workflow will be rendered. - /// - Parameter key: A string that uniquely identifies this workflow. - /// - /// - Returns: The `Rendering` generated by the workflow. - public func rendered(with context: RenderContext, key: String = "") -> Rendering { - // Convenience for workflow that have no output allowing them to be rendered with any context - - return asAnyWorkflow() - .render( - context: context, - key: key, - outputMap: { _ -> AnyWorkflowAction in } - ) - } -} diff --git a/swift/Workflow/Sources/Debugging.swift b/swift/Workflow/Sources/Debugging.swift deleted file mode 100644 index 5e4bf8ee4..000000000 --- a/swift/Workflow/Sources/Debugging.swift +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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. - */ - -public struct WorkflowUpdateDebugInfo: Codable, Equatable { - public var workflowType: String - public var kind: Kind - - internal init(workflowType: String, kind: Kind) { - self.workflowType = workflowType - self.kind = kind - } -} - -extension WorkflowUpdateDebugInfo { - public indirect enum Kind: Equatable { - case didUpdate(source: Source) - case childDidUpdate(WorkflowUpdateDebugInfo) - } -} - -extension WorkflowUpdateDebugInfo.Kind: Codable { - enum CodingKeys: String, CodingKey { - case type - case source - case childUpdate - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - - switch self { - case let .didUpdate(source): - try container.encode("didUpdate", forKey: .type) - try container.encode(source, forKey: .source) - case let .childDidUpdate(info): - try container.encode("childDidUpdate", forKey: .type) - try container.encode(info, forKey: .childUpdate) - } - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - struct MalformedDataError: Error {} - - let typeString = try container.decode(String.self, forKey: .type) - - switch typeString { - case "didUpdate": - let source = try container.decode(WorkflowUpdateDebugInfo.Source.self, forKey: .source) - self = .didUpdate(source: source) - case "childDidUpdate": - let childUpdate = try container.decode(WorkflowUpdateDebugInfo.self, forKey: .childUpdate) - self = .childDidUpdate(childUpdate) - default: - throw MalformedDataError() - } - } -} - -extension WorkflowUpdateDebugInfo { - public indirect enum Source: Equatable { - case external - case worker - case sideEffect - case subtree(WorkflowUpdateDebugInfo) - } -} - -extension WorkflowUpdateDebugInfo.Source: Codable { - enum CodingKeys: String, CodingKey { - case type - case debugInfo - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - - switch self { - case .external: - try container.encode("external", forKey: .type) - case .worker: - try container.encode("worker", forKey: .type) - case let .subtree(debugInfo): - try container.encode("subtree", forKey: .type) - try container.encode(debugInfo, forKey: .debugInfo) - case .sideEffect: - try container.encode("side-effect", forKey: .type) - } - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - struct MalformedDataError: Error {} - - let typeString = try container.decode(String.self, forKey: .type) - - switch typeString { - case "external": - self = .external - case "worker": - self = .worker - case "subtree": - let debugInfo = try container.decode(WorkflowUpdateDebugInfo.self, forKey: .debugInfo) - self = .subtree(debugInfo) - case "side-effect": - self = .sideEffect - default: - throw MalformedDataError() - } - } -} - -public struct WorkflowHierarchyDebugSnapshot: Codable, Equatable { - public var workflowType: String - public var stateDescription: String - public var children: [Child] - - init(workflowType: String, stateDescription: String, children: [Child] = []) { - self.workflowType = workflowType - self.stateDescription = stateDescription - self.children = children - } -} - -extension WorkflowHierarchyDebugSnapshot { - public struct Child: Codable, Equatable { - public var key: String - public var snapshot: WorkflowHierarchyDebugSnapshot - - init(key: String, snapshot: WorkflowHierarchyDebugSnapshot) { - self.key = key - self.snapshot = snapshot - } - } -} diff --git a/swift/Workflow/Sources/DispatchQueue+Workflow.swift b/swift/Workflow/Sources/DispatchQueue+Workflow.swift deleted file mode 100644 index 13330770f..000000000 --- a/swift/Workflow/Sources/DispatchQueue+Workflow.swift +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 ReactiveSwift - -extension DispatchQueue { - static let workflowExecution: DispatchQueue = .main -} - -extension QueueScheduler { - static let workflowExecution: QueueScheduler = QueueScheduler( - qos: .userInteractive, - name: "com.squareup.workflow", - targeting: DispatchQueue.workflowExecution - ) -} diff --git a/swift/Workflow/Sources/Lifetime.swift b/swift/Workflow/Sources/Lifetime.swift deleted file mode 100644 index 07e0441be..000000000 --- a/swift/Workflow/Sources/Lifetime.swift +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 - -/// Represents the lifetime of an object. -/// -/// Once ended, the `onEnded` closure is called. -public final class Lifetime { - /// Hook to clean-up after end of `lifetime`. - public func onEnded(_ action: @escaping () -> Void) { - assert(!hasEnded, "Lifetime used after being ended.") - onEndedActions.append(action) - } - - public private(set) var hasEnded: Bool = false - private var onEndedActions: [() -> Void] = [] - - deinit { - end() - } - - func end() { - guard !hasEnded else { - return - } - hasEnded = true - onEndedActions.forEach { $0() } - } -} diff --git a/swift/Workflow/Sources/RenderContext.swift b/swift/Workflow/Sources/RenderContext.swift deleted file mode 100644 index 47fa6f837..000000000 --- a/swift/Workflow/Sources/RenderContext.swift +++ /dev/null @@ -1,175 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 ReactiveSwift - -/// `RenderContext` is the composition point for the workflow tree. -/// -/// During a render pass, a workflow may want to defer to a child -/// workflow to render some portion of its content. For example, -/// a workflow that renders to a split-screen view model might -/// delegate to child A for the left side, and child B for the right -/// side view models. Nesting allows for a fractal tree that is constructed -/// out of many small parts. -/// -/// If a parent wants to delegate to a child workflow, it must first -/// create an instance of that workflow. This can be thought of as the -/// model of the child workflow. It does not contain any active state, -/// it simply contains the data necessary to create or update a workflow -/// node. -/// -/// The parent then calls `render(workflow:outputMap:)` -/// with two values: -/// - The child workflow. -/// - A closure that transforms the child's output events into the parent's -/// `Event` type so that the parent can respond to events generated by -/// the child. -/// -/// If the parent had previously rendered a child of the same type, the existing -/// child workflow node is updated. -/// -/// If the parent had not rendered a child of the same type in the previous -/// render pass, a new child workflow node is generated. -/// -/// The infrastructure then performs a render pass on the child to obtain its -/// `Rendering` value, which is then returned to the caller. -public class RenderContext: RenderContextType { - private(set) var isValid = true - - // Ensure that this class can never be initialized externally - private init() {} - - /// Creates or updates a child workflow of the given type, performs a render - /// pass, and returns the result. - /// - /// Note that it is a programmer error to render two instances of a given workflow type with the same `key` - /// during the same render pass. - /// - /// - Parameter workflow: The child workflow to be rendered. - /// - Parameter outputMap: A closure that transforms the child's output type into `Action`. - /// - Parameter key: A string that uniquely identifies this child. - /// - /// - Returns: The `Rendering` result of the child's `render` method. - public func render(workflow: Child, key: String, outputMap: @escaping (Child.Output) -> Action) -> Child.Rendering where Child: Workflow, Action: WorkflowAction, WorkflowType == Action.WorkflowType { - fatalError() - } - - public func makeSink(of actionType: Action.Type) -> Sink where Action: WorkflowAction, Action.WorkflowType == WorkflowType { - fatalError() - } - - public func awaitResult(for worker: W, outputMap: @escaping (W.Output) -> Action) where W: Worker, Action: WorkflowAction, WorkflowType == Action.WorkflowType { - fatalError() - } - - /// Execute a side-effect action. - /// - /// Note that it is a programmer error to run two side-effects with the same `key` - /// during the same render pass. - /// - /// `action` will be executed the first time a side-effect is run with a given `key`. - /// `runSideEffect` calls with a given `key` on subsequent renders are ignored. - /// - /// If after a render pass, a side-effect with a `key` that was previously used is not used, - /// it's lifetime ends and the `Lifetime` object's `onEnded` closure will be called. - /// - /// - Parameters: - /// - key: represents the block of work that needs to be executed. - /// - action: a block of work that will be executed. - public func runSideEffect(key: AnyHashable, action: (Lifetime) -> Void) { - fatalError() - } - - final func invalidate() { - isValid = false - } - - // API to allow custom context implementations to power a render context - static func make(implementation: T) -> RenderContext where T.WorkflowType == WorkflowType { - return ConcreteRenderContext(implementation) - } - - // Private subclass that forwards render calls to a wrapped implementation. This is the only `RenderContext` class - // that is ever instantiated. - private final class ConcreteRenderContext: RenderContext where WorkflowType == T.WorkflowType { - let implementation: T - - init(_ implementation: T) { - self.implementation = implementation - super.init() - } - - override func render(workflow: Child, key: String, outputMap: @escaping (Child.Output) -> Action) -> Child.Rendering where WorkflowType == Action.WorkflowType, Child: Workflow, Action: WorkflowAction { - assertStillValid() - return implementation.render(workflow: workflow, key: key, outputMap: outputMap) - } - - override func makeSink(of actionType: Action.Type) -> Sink where WorkflowType == Action.WorkflowType, Action: WorkflowAction { - return implementation.makeSink(of: actionType) - } - - override func runSideEffect(key: AnyHashable, action: (_ lifetime: Lifetime) -> Void) { - assertStillValid() - implementation.runSideEffect(key: key, action: action) - } - - override func awaitResult(for worker: W, outputMap: @escaping (W.Output) -> Action) where W: Worker, Action: WorkflowAction, WorkflowType == Action.WorkflowType { - assertStillValid() - implementation.awaitResult(for: worker, outputMap: outputMap) - } - - private func assertStillValid() { - assert(isValid, "A `RenderContext` instance was used outside of the workflow's `render` method. It is a programmer error to capture a context in a closure or otherwise cause it to be used outside of the `render` method.") - } - } -} - -internal protocol RenderContextType: AnyObject { - associatedtype WorkflowType: Workflow - - func render(workflow: Child, key: String, outputMap: @escaping (Child.Output) -> Action) -> Child.Rendering where Child: Workflow, Action: WorkflowAction, Action.WorkflowType == WorkflowType - - func makeSink(of actionType: Action.Type) -> Sink where Action: WorkflowAction, Action.WorkflowType == WorkflowType - - func awaitResult(for worker: W, outputMap: @escaping (W.Output) -> Action) where W: Worker, Action: WorkflowAction, Action.WorkflowType == WorkflowType - - func runSideEffect(key: AnyHashable, action: (_ lifetime: Lifetime) -> Void) -} - -extension RenderContext { - public func makeSink(of eventType: Event.Type, onEvent: @escaping (Event, inout WorkflowType.State) -> WorkflowType.Output?) -> Sink { - return makeSink(of: AnyWorkflowAction.self) - .contraMap { event in - AnyWorkflowAction { state in - onEvent(event, &state) - } - } - } -} - -extension RenderContext { - public func awaitResult(for worker: W) where W: Worker, W.Output: WorkflowAction, WorkflowType == W.Output.WorkflowType { - awaitResult(for: worker, outputMap: { $0 }) - } - - public func awaitResult(for worker: W, onOutput: @escaping (W.Output, inout WorkflowType.State) -> WorkflowType.Output?) where W: Worker { - awaitResult(for: worker) { output in - AnyWorkflowAction { state in - onOutput(output, &state) - } - } - } -} diff --git a/swift/Workflow/Sources/SignalWorker.swift b/swift/Workflow/Sources/SignalWorker.swift deleted file mode 100644 index 6ba72c1f9..000000000 --- a/swift/Workflow/Sources/SignalWorker.swift +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 ReactiveSwift - -/// A `Worker` that wraps a `Signal` -public struct SignalWorker: Worker { - let key: Key - let signal: Signal - - public init(key: Key, signal: Signal) { - self.key = key - self.signal = signal - } - - public func run() -> SignalProducer { - return SignalProducer(signal) - } - - public func isEquivalent(to otherWorker: SignalWorker) -> Bool { - return key == otherWorker.key - } -} - -extension Signal where Error == Never { - public func asWorker(key: Key) -> SignalWorker { - return SignalWorker(key: key, signal: self) - } -} diff --git a/swift/Workflow/Sources/Sink.swift b/swift/Workflow/Sources/Sink.swift deleted file mode 100644 index 573478345..000000000 --- a/swift/Workflow/Sources/Sink.swift +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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. - */ - -/// Sink is a type that receives incoming values (commonly events or `WorkflowAction`) -/// -/// Use `RenderContext.makeSink` to create instances. -public struct Sink { - private let onValue: (Value) -> Void - - /// Initializes a new sink with the given closure. - public init(_ onValue: @escaping (Value) -> Void) { - self.onValue = onValue - } - - /// Sends a new event into the sink. - /// - /// - Parameter event: The value to send into the sink. - public func send(_ value: Value) { - onValue(value) - } - - /// Generates a new sink of type NewValue. - /// - /// Given a `transform` closure, the following code is functionally equivalent: - /// - /// ``` - /// sink.send(transform(value)) - /// ``` - /// ``` - /// sink.contraMap(transform).send(value) - /// ``` - /// - /// **Trivia**: Why is this called `contraMap`? - /// - `map` turns `Type` into `Type` via `(T)->U`. - /// - `contraMap` turns `Type` into `Type` via `(U)->T` - /// - /// Another way to think about this is: `map` transforms a type by changing the - /// output types of its API, while `contraMap` transforms a type by changing the - /// *input* types of its API. - /// - /// - Parameter transform: An escaping closure that transforms `T` into `Event`. - public func contraMap(_ transform: @escaping (NewValue) -> Value) -> Sink { - return Sink { value in - self.send(transform(value)) - } - } -} diff --git a/swift/Workflow/Sources/SubtreeManager.swift b/swift/Workflow/Sources/SubtreeManager.swift deleted file mode 100644 index 69d6ff913..000000000 --- a/swift/Workflow/Sources/SubtreeManager.swift +++ /dev/null @@ -1,564 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 Dispatch -import ReactiveSwift - -extension WorkflowNode { - /// Manages the subtree of a workflow. Specifically, this type encapsulates the logic required to update and manage - /// the lifecycle of nested workflows across multiple render passes. - internal final class SubtreeManager { - internal var onUpdate: ((Output) -> Void)? - - /// Sinks from the outside world (i.e. UI) - private var eventPipes: [EventPipe] = [] - - /// Reusable sinks from the previous render pass - private var previousSinks: [ObjectIdentifier: AnyReusableSink] = [:] - - /// The current array of children - internal private(set) var childWorkflows: [ChildKey: AnyChildWorkflow] = [:] - - /// The current array of workers - internal private(set) var childWorkers: [AnyChildWorker] = [] - - /// The current array of side-effects - internal private(set) var sideEffectLifetimes: [AnyHashable: SideEffectLifetime] = [:] - - init() {} - - /// Performs an update pass using the given closure. - func render(_ actions: (RenderContext) -> Rendering) -> Rendering { - /// Invalidate the previous action handlers. - for eventPipe in eventPipes { - eventPipe.invalidate() - } - - /// Create a workflow context containing the existing children - let context = Context( - previousSinks: previousSinks, - originalChildWorkflows: childWorkflows, - originalChildWorkers: childWorkers, - originalSideEffectLifetimes: sideEffectLifetimes - ) - - let wrapped = RenderContext.make(implementation: context) - - /// Pass the context into the closure to allow a render to take place - let rendering = actions(wrapped) - - wrapped.invalidate() - - /// After the render is complete, assign children using *only the children that were used during the render - /// pass.* This means that any pre-existing children that were *not* used during the render pass are removed - /// as a result of this call to `render`. - childWorkflows = context.usedChildWorkflows - childWorkers = context.usedChildWorkers - sideEffectLifetimes = context.usedSideEffectLifetimes - - /// Captured the reusable sinks from this render pass. - previousSinks = context.sinkStore.usedSinks - - /// Capture all the pipes to be enabled after render completes. - eventPipes = context.eventPipes - eventPipes.append(contentsOf: context.sinkStore.eventPipes) - - /// Set all event pipes to `pending`. - eventPipes.forEach { $0.setPending() } - - /// Return the rendered result - return rendering - } - - /// Enable the eventPipes for the previous rendering. The eventPipes are not valid until this has - /// be called. If is an error to call this twice without generating a new rendering. - func enableEvents() { - /// Check for queued events. If there are any, apply the first and yield to the next render loop. - let queuedEvents = eventPipes.compactMap { pipe in - pipe.pendingOutput() - } - if !queuedEvents.isEmpty { - handle(output: queuedEvents[0]) - return - } - - /// Enable all action pipes. - for eventPipe in eventPipes { - eventPipe.enable { [weak self] output in - self?.handle(output: output) - } - } - - /// Enable all child workflows. - for child in childWorkflows { - child.value.enableEvents() - } - } - - func makeDebugSnapshot() -> [WorkflowHierarchyDebugSnapshot.Child] { - return childWorkflows - .sorted(by: { (lhs, rhs) -> Bool in - lhs.key.key < rhs.key.key - }) - .map { - WorkflowHierarchyDebugSnapshot.Child( - key: $0.key.key, - snapshot: $0.value.makeDebugSnapshot() - ) - } - } - - private func handle(output: Output) { - onUpdate?(output) - } - } -} - -extension WorkflowNode.SubtreeManager { - enum Output { - case update(AnyWorkflowAction, source: WorkflowUpdateDebugInfo.Source) - case childDidUpdate(WorkflowUpdateDebugInfo) - } -} - -// MARK: - Render Context - -extension WorkflowNode.SubtreeManager { - /// The workflow context implementation used by the subtree manager. - fileprivate final class Context: RenderContextType { - internal private(set) var eventPipes: [EventPipe] - - internal private(set) var sinkStore: SinkStore - - private let originalChildWorkflows: [ChildKey: AnyChildWorkflow] - internal private(set) var usedChildWorkflows: [ChildKey: AnyChildWorkflow] - - private let originalChildWorkers: [AnyChildWorker] - internal private(set) var usedChildWorkers: [AnyChildWorker] - - private let originalSideEffectLifetimes: [AnyHashable: SideEffectLifetime] - internal private(set) var usedSideEffectLifetimes: [AnyHashable: SideEffectLifetime] - - internal init( - previousSinks: [ObjectIdentifier: AnyReusableSink], - originalChildWorkflows: [ChildKey: AnyChildWorkflow], - originalChildWorkers: [AnyChildWorker], - originalSideEffectLifetimes: [AnyHashable: SideEffectLifetime] - ) { - self.eventPipes = [] - - self.sinkStore = SinkStore(previousSinks: previousSinks) - - self.originalChildWorkflows = originalChildWorkflows - self.usedChildWorkflows = [:] - - self.originalChildWorkers = originalChildWorkers - self.usedChildWorkers = [] - - self.originalSideEffectLifetimes = originalSideEffectLifetimes - self.usedSideEffectLifetimes = [:] - } - - func render(workflow: Child, key: String, outputMap: @escaping (Child.Output) -> Action) -> Child.Rendering where Child: Workflow, Action: WorkflowAction, WorkflowType == Action.WorkflowType { - /// A unique key used to identify this child workflow - let childKey = ChildKey(childType: Child.self, key: key) - - /// If the key already exists in `used`, than a workflow of the same type has been rendered multiple times - /// during this render pass with the same key. This is not allowed. - guard usedChildWorkflows[childKey] == nil else { - fatalError("Child workflows of the same type must be given unique keys. Duplicate workflows of type \(Child.self) were encountered with the key \"\(key)\" in \(WorkflowType.self)") - } - - let child: ChildWorkflow - let eventPipe = EventPipe() - eventPipes.append(eventPipe) - - /// See if we can - if let existing = originalChildWorkflows[childKey] { - /// Cast the untyped child into a specific typed child. Because our children are keyed by their workflow - /// type, this should never fail. - guard let existing = existing as? ChildWorkflow else { - fatalError("ChildKey class type does not match the underlying workflow type.") - } - - /// Update the existing child - existing.update( - workflow: workflow, - outputMap: { AnyWorkflowAction(outputMap($0)) }, - eventPipe: eventPipe - ) - child = existing - } else { - /// We could not find an existing child matching the given child key, so we will generate a new child. - /// This spins up a new workflow node, etc to host the newly created child. - child = ChildWorkflow( - workflow: workflow, - outputMap: { AnyWorkflowAction(outputMap($0)) }, - eventPipe: eventPipe - ) - } - - /// Store the resolved child in `used`. This allows us to a) hold on to any used children after this render - /// pass, and b) ensure that we never allow the use of a given workflow type with identical keys. - usedChildWorkflows[childKey] = child - return child.render() - } - - func makeSink(of actionType: Action.Type) -> Sink where Action: WorkflowAction, WorkflowType == Action.WorkflowType { - let reusableSink = sinkStore.findOrCreate(actionType: Action.self) - - let signpostRef = SignpostRef() - - let sink = Sink { action in - WorkflowLogger.logSinkEvent(ref: signpostRef, action: action) - - reusableSink.handle(action: action) - } - - return sink - } - - func awaitResult(for worker: W, outputMap: @escaping (W.Output) -> Action) where W: Worker, Action: WorkflowAction, WorkflowType == Action.WorkflowType { - let outputMap = { AnyWorkflowAction(outputMap($0)) } - let eventPipe = EventPipe() - eventPipes.append(eventPipe) - - if let existingWorker = originalChildWorkers - .compactMap({ $0 as? ChildWorker }) - .first(where: { $0.worker.isEquivalent(to: worker) }) { - existingWorker.update(outputMap: outputMap, eventPipe: eventPipe) - usedChildWorkers.append(existingWorker) - } else { - let newChildWorker = ChildWorker(worker: worker, outputMap: outputMap, eventPipe: eventPipe) - usedChildWorkers.append(newChildWorker) - } - } - - func runSideEffect(key: AnyHashable, action: (Lifetime) -> Void) { - if let existingSideEffect = originalSideEffectLifetimes[key] { - usedSideEffectLifetimes[key] = existingSideEffect - } else { - let sideEffectLifetime = SideEffectLifetime() - action(sideEffectLifetime.lifetime) - usedSideEffectLifetimes[key] = sideEffectLifetime - } - } - } -} - -// MARK: - Reusable Sink - -extension WorkflowNode.SubtreeManager { - fileprivate struct SinkStore { - var eventPipes: [EventPipe] { - return usedSinks.values.map { reusableSink -> EventPipe in - reusableSink.eventPipe - } - } - - private var previousSinks: [ObjectIdentifier: AnyReusableSink] - private(set) var usedSinks: [ObjectIdentifier: AnyReusableSink] - - init(previousSinks: [ObjectIdentifier: AnyReusableSink]) { - self.previousSinks = previousSinks - self.usedSinks = [:] - } - - mutating func findOrCreate(actionType: Action.Type) -> ReusableSink { - let key = ObjectIdentifier(actionType) - - let reusableSink: ReusableSink - - if let previousSink = previousSinks.removeValue(forKey: key) as? ReusableSink { - // Reuse a previous sink, creating a new event pipe to send the action through. - previousSink.eventPipe = EventPipe() - reusableSink = previousSink - } else if let usedSink = usedSinks[key] as? ReusableSink { - // Multiple sinks using the same backing sink. - reusableSink = usedSink - } else { - // Create a new reusable sink. - reusableSink = ReusableSink() - } - - usedSinks[key] = reusableSink - - return reusableSink - } - } - - /// Type-erased base class for reusable sinks. - fileprivate class AnyReusableSink { - var eventPipe: EventPipe - - init() { - self.eventPipe = EventPipe() - } - } - - fileprivate final class ReusableSink: AnyReusableSink where Action.WorkflowType == WorkflowType { - func handle(action: Action) { - let output = Output.update(AnyWorkflowAction(action), source: .external) - - eventPipe.handle(event: output) - } - } -} - -// MARK: - EventPipe - -extension WorkflowNode.SubtreeManager { - fileprivate final class EventPipe { - var validationState: ValidationState - enum ValidationState { - case preparing - case pending - case queued(Output) - case valid(handler: (Output) -> Void) - case invalid - } - - init() { - self.validationState = .preparing - } - - func handle(event: Output) { - if #available(iOS 10.0, *) { - dispatchPrecondition(condition: .onQueue(DispatchQueue.workflowExecution)) - } - - switch validationState { - case .preparing: - fatalError("[\(WorkflowType.self)] Sink sent an action inside `render`. Sinks are not valid until `render` has completed.") - - case .pending: - validationState = .queued(event) - - case .queued: - fatalError("[\(WorkflowType.self)] Action sent to pipe while already in the `queueing` state.") - - case let .valid(handler: handler): - handler(event) - - case .invalid: - fatalError("[\(WorkflowType.self)] Sink sent an action after it was invalidated. Sinks can only be used for a single valid `Rendering`.") - } - } - - func setPending() { - guard case .preparing = validationState else { - fatalError("Attempted to `setPending` an EventPipe that was not in the preparing state.") - } - validationState = .pending - } - - func pendingOutput() -> Output? { - if case let .queued(output) = validationState { - return output - } else { - return nil - } - } - - func enable(with handler: @escaping (Output) -> Void) { - guard case .pending = validationState else { - fatalError("EventPipe can only be enabled from the `pending` state") - } - validationState = .valid(handler: handler) - } - - func invalidate() { - validationState = .invalid - } - } -} - -// MARK: - ChildKey - -extension WorkflowNode.SubtreeManager { - struct ChildKey: Hashable { - var childType: Any.Type - var key: String - - init(childType: T.Type, key: String) { - self.childType = childType - self.key = key - } - - func hash(into hasher: inout Hasher) { - hasher.combine(ObjectIdentifier(childType)) - hasher.combine(key) - } - - static func == (lhs: ChildKey, rhs: ChildKey) -> Bool { - return lhs.childType == rhs.childType - && lhs.key == rhs.key - } - } -} - -// MARK: - Workers - -extension WorkflowNode.SubtreeManager { - /// Abstract base class for running children in the subtree. - internal class AnyChildWorker { - fileprivate var eventPipe: EventPipe - - fileprivate init(eventPipe: EventPipe) { - self.eventPipe = eventPipe - } - } - - fileprivate final class ChildWorker: AnyChildWorker { - let worker: W - - let signalProducer: SignalProducer - - private var outputMap: (W.Output) -> AnyWorkflowAction - - private let (lifetime, token) = ReactiveSwift.Lifetime.make() - - init(worker: W, outputMap: @escaping (W.Output) -> AnyWorkflowAction, eventPipe: EventPipe) { - self.worker = worker - self.signalProducer = worker.run() - self.outputMap = outputMap - super.init(eventPipe: eventPipe) - - let signpostRef = SignpostRef() - WorkflowLogger.logWorkerStartedRunning(ref: signpostRef, workerType: W.self) - - signalProducer - .take(during: lifetime) - .observe(on: QueueScheduler.workflowExecution) - .start { [weak self] event in - switch event { - case let .value(output): - WorkflowLogger.logWorkerOutput(ref: signpostRef, workerType: W.self) - - self?.handle(output: output) - case .completed: - WorkflowLogger.logWorkerFinishedRunning(ref: signpostRef, status: "Completed") - case .interrupted: - WorkflowLogger.logWorkerFinishedRunning(ref: signpostRef, status: "Interrupted") - case .failed: - WorkflowLogger.logWorkerFinishedRunning(ref: signpostRef, status: "Failed") - } - } - } - - func update(outputMap: @escaping (W.Output) -> AnyWorkflowAction, eventPipe: EventPipe) { - self.outputMap = outputMap - self.eventPipe = eventPipe - } - - private func handle(output: W.Output) { - let output = Output.update( - outputMap(output), - source: .worker - ) - eventPipe.handle(event: output) - } - } -} - -// MARK: - Child Workflows - -extension WorkflowNode.SubtreeManager { - /// Abstract base class for running children in the subtree. - internal class AnyChildWorkflow { - fileprivate var eventPipe: EventPipe - - fileprivate init(eventPipe: EventPipe) { - self.eventPipe = eventPipe - } - - func enableEvents() { - fatalError() - } - - func makeDebugSnapshot() -> WorkflowHierarchyDebugSnapshot { - fatalError() - } - } - - fileprivate final class ChildWorkflow: AnyChildWorkflow { - private let node: WorkflowNode - private var outputMap: (W.Output) -> AnyWorkflowAction - - private let (lifetime, token) = ReactiveSwift.Lifetime.make() - - init(workflow: W, outputMap: @escaping (W.Output) -> AnyWorkflowAction, eventPipe: EventPipe) { - self.outputMap = outputMap - self.node = WorkflowNode(workflow: workflow) - - super.init(eventPipe: eventPipe) - - node.onOutput = { [weak self] output in - self?.handle(workflowOutput: output) - } - } - - override func enableEvents() { - node.enableEvents() - } - - func render() -> W.Rendering { - return node.render() - } - - func update(workflow: W, outputMap: @escaping (W.Output) -> AnyWorkflowAction, eventPipe: EventPipe) { - self.outputMap = outputMap - self.eventPipe = eventPipe - node.update(workflow: workflow) - } - - private func handle(workflowOutput: WorkflowNode.Output) { - let output: Output - - if let outputEvent = workflowOutput.outputEvent { - output = Output.update( - outputMap(outputEvent), - source: .subtree(workflowOutput.debugInfo) - ) - } else { - output = Output.childDidUpdate(workflowOutput.debugInfo) - } - - eventPipe.handle(event: output) - } - - override func makeDebugSnapshot() -> WorkflowHierarchyDebugSnapshot { - return node.makeDebugSnapshot() - } - } -} - -// MARK: - Side Effects - -extension WorkflowNode.SubtreeManager { - internal class SideEffectLifetime { - fileprivate let lifetime: Lifetime - - fileprivate init() { - self.lifetime = Lifetime() - } - - deinit { - // Explicitly end the lifetime in case someone retained it from outside - lifetime.end() - } - } -} diff --git a/swift/Workflow/Sources/Worker.swift b/swift/Workflow/Sources/Worker.swift deleted file mode 100644 index 39c61354e..000000000 --- a/swift/Workflow/Sources/Worker.swift +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 ReactiveSwift - -/// Workers define a unit of asynchronous work. -/// -/// During a render pass, a workflow can ask the context to await the result of a worker. -/// -/// When this occurs, the context checks to see if there is already a running worker of the same type. -/// If there is, and if the workers are 'equivalent', the context leaves the existing worker running. -/// -/// If there is not an existing worker of this type, the context will kick off the new worker (via `run`). -public protocol Worker { - /// The type of output events returned by this worker. - associatedtype Output - - /// Returns a signal producer to execute the work represented by this worker. - func run() -> SignalProducer - - /// Returns `true` if the other worker should be considered equivalent to `self`. Equivalence should take into - /// account whatever data is meaninful to the task. For example, a worker that loads a user account from a server - /// would not be equivalent to another worker with a different user ID. - func isEquivalent(to otherWorker: Self) -> Bool -} - -extension Worker where Self: Equatable { - public func isEquivalent(to otherWorker: Self) -> Bool { - return self == otherWorker - } -} diff --git a/swift/Workflow/Sources/Workflow.swift b/swift/Workflow/Sources/Workflow.swift deleted file mode 100644 index 85fcff7ef..000000000 --- a/swift/Workflow/Sources/Workflow.swift +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 ReactiveSwift - -/// Defines a node in the workflow tree. -/// -/// *** -/// **Initialization and Updating** -/// *** -/// -/// A workflow node comes into existence after its parent produces -/// an instance of that workflow and uses it during a render pass (see the -/// `render` method for more details). -/// -/// - If this is the first time the parent has rendered a child of -/// this type, a new workflow node is created. The workflow -/// passed in from the parent will be used to invoke -/// `initialState()` to obtain an initial state. -/// -/// - If the parent had previously rendered a child of this type, the -/// existing workflow node will be updated. -/// `workflowDidChange(from:state:)` will be invoked -/// to allow the workflow to respond to the change. -/// -/// *** -/// **Render** -/// *** -/// After a workflow node has been created, or any time its state changes, -/// a render pass occurs. The render pass takes the workflow that was passed -/// down from the parent along with the current state and generates a value -/// of type `Rendering`. In a common case, a workflow might render to a screen -/// model for display. -/// -/// ``` -/// func render(state: State, context: RenderContext) -> MyScreenModel { -/// return MyScreenModel() -/// } -/// ``` -/// -public protocol Workflow: AnyWorkflowConvertible { - /// Defines the state that is managed by this workflow. - associatedtype State - - /// `Output` defines the type that can be emitted as output events. - associatedtype Output = Never - - /// `Rendering` is the type that is produced by the `render` method: it - /// is commonly a view / screen model. - associatedtype Rendering - - /// This method is invoked once when a workflow node comes into existence. - /// - /// - Returns: The initial state for the workflow. - func makeInitialState() -> State - - /// Called when a new workflow is passed down from the parent to an existing workflow node. - /// - /// - Parameter previousWorkflow: The workflow before the update. - /// - Parameter state: The current state. - func workflowDidChange(from previousWorkflow: Self, state: inout State) - - /// Called to "render" the current state into `Rendering`. A workflow's `Rendering` type is commonly a view or - /// screen model. - /// - /// - Parameter state: The current state. - /// - Parameter context: The workflow context is the composition point for the workflow tree. To use a nested - /// workflow, instantiate it based on the current state. The newly instantiated workflow is - /// then used to invoke `context.render(_ workflow:)`, which returns the child's `Rendering` - /// type after creating or updating the nested workflow. - func render(state: State, context: RenderContext) -> Rendering -} - -extension Workflow { - public func workflowDidChange(from previousWorkflow: Self, state: inout State) {} -} - -/// When State is Void, provide empty `makeInitialState` and `workflowDidChange` -/// implementations, making a “stateless workflow”. -extension Workflow where State == Void { - public func makeInitialState() -> State { - return () - } - - public func workflowDidChange(from previousWorkflow: Self, state: inout State) {} -} - -extension Workflow { - public func asAnyWorkflow() -> AnyWorkflow { - return AnyWorkflow(self) - } -} diff --git a/swift/Workflow/Sources/WorkflowAction.swift b/swift/Workflow/Sources/WorkflowAction.swift deleted file mode 100644 index 9c9be1cfb..000000000 --- a/swift/Workflow/Sources/WorkflowAction.swift +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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. - */ - -/// Conforming types represent an action that advances a workflow. When applied, an action emits the next -/// state and / or output for the workflow. -public protocol WorkflowAction { - /// The type of workflow that this action can be applied to. - associatedtype WorkflowType: Workflow - - /// Applies this action to a given state of the workflow, optionally returning an output event. - /// - /// - Parameter state: The current state of the workflow. The state is passed as an `inout` param, allowing actions - /// to modify state during application. - /// - /// - Returns: An optional output event for the workflow. If an output event is returned, it will be passed up - /// the workflow hierarchy to this workflow's parent. - func apply(toState state: inout WorkflowType.State) -> WorkflowType.Output? -} - -/// A type-erased workflow action. -/// -/// The `AnyWorkflowAction` type forwards `apply` to an underlying workflow action, hiding its specific underlying type, -/// or to a closure that implements the `apply` logic. -public struct AnyWorkflowAction: WorkflowAction { - private let _apply: (inout WorkflowType.State) -> WorkflowType.Output? - - /// Creates a type-erased workflow action that wraps the given instance. - /// - /// - Parameter base: A workflow action to wrap. - public init(_ base: E) where E: WorkflowAction, E.WorkflowType == WorkflowType { - if let anyEvent = base as? AnyWorkflowAction { - self = anyEvent - return - } - self._apply = { return base.apply(toState: &$0) } - } - - /// Creates a type-erased workflow action with the given `apply` implementation. - /// - /// - Parameter apply: the apply function for the resulting action. - public init(_ apply: @escaping (inout WorkflowType.State) -> WorkflowType.Output?) { - self._apply = apply - } - - public func apply(toState state: inout WorkflowType.State) -> WorkflowType.Output? { - return _apply(&state) - } -} - -extension AnyWorkflowAction { - /// Creates a type-erased workflow action that simply sends the given output event. - /// - /// - Parameter output: The output event to send when this action is applied. - public init(sendingOutput output: WorkflowType.Output) { - self = AnyWorkflowAction { state in - output - } - } - - /// Creates a type-erased workflow action that does nothing (it leaves state unchanged and does not emit an output - /// event). - public static var noAction: AnyWorkflowAction { - return AnyWorkflowAction { state in - nil - } - } -} diff --git a/swift/Workflow/Sources/WorkflowHost.swift b/swift/Workflow/Sources/WorkflowHost.swift deleted file mode 100644 index b1029f670..000000000 --- a/swift/Workflow/Sources/WorkflowHost.swift +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 ReactiveSwift - -/// Defines a type that receives debug information about a running workflow hierarchy. -public protocol WorkflowDebugger { - /// Called once when the workflow hierarchy initializes. - /// - /// - Parameter snapshot: Debug information about the workflow hierarchy. - func didEnterInitialState(snapshot: WorkflowHierarchyDebugSnapshot) - - /// Called when an update occurs anywhere within the workflow hierarchy. - /// - /// - Parameter snapshot: Debug information about the workflow hierarchy *after* the update. - /// - Parameter updateInfo: Information about the update. - func didUpdate(snapshot: WorkflowHierarchyDebugSnapshot, updateInfo: WorkflowUpdateDebugInfo) -} - -/// Manages an active workflow hierarchy. -public final class WorkflowHost { - private let debugger: WorkflowDebugger? - - private let (outputEvent, outputEventObserver) = Signal.pipe() - - private let rootNode: WorkflowNode - - private let mutableRendering: MutableProperty - - /// Represents the `Rendering` produced by the root workflow in the hierarchy. New `Rendering` values are produced - /// as state transitions occur within the hierarchy. - public let rendering: Property - - /// Initializes a new host with the given workflow at the root. - /// - /// - Parameter workflow: The root workflow in the hierarchy - /// - Parameter debugger: An optional debugger. If provided, the host will notify the debugger of updates - /// to the workflow hierarchy as state transitions occur. - public init(workflow: WorkflowType, debugger: WorkflowDebugger? = nil) { - self.debugger = debugger - - self.rootNode = WorkflowNode(workflow: workflow) - - self.mutableRendering = MutableProperty(rootNode.render()) - self.rendering = Property(mutableRendering) - rootNode.enableEvents() - - debugger?.didEnterInitialState(snapshot: rootNode.makeDebugSnapshot()) - - rootNode.onOutput = { [weak self] output in - self?.handle(output: output) - } - } - - /// Update the input for the workflow. Will cause a render pass. - public func update(workflow: WorkflowType) { - rootNode.update(workflow: workflow) - - // Treat the update as an "output" from the workflow originating from an external event to force a render pass. - let output = WorkflowNode.Output( - outputEvent: nil, - debugInfo: WorkflowUpdateDebugInfo( - workflowType: "\(WorkflowType.self)", - kind: .didUpdate(source: .external) - ) - ) - handle(output: output) - } - - private func handle(output: WorkflowNode.Output) { - mutableRendering.value = rootNode.render() - - if let outputEvent = output.outputEvent { - outputEventObserver.send(value: outputEvent) - } - - debugger?.didUpdate( - snapshot: rootNode.makeDebugSnapshot(), - updateInfo: output.debugInfo - ) - - rootNode.enableEvents() - } - - /// A signal containing output events emitted by the root workflow in the hierarchy. - public var output: Signal { - return outputEvent - } -} diff --git a/swift/Workflow/Sources/WorkflowLogger.swift b/swift/Workflow/Sources/WorkflowLogger.swift deleted file mode 100644 index 6667c3d9b..000000000 --- a/swift/Workflow/Sources/WorkflowLogger.swift +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 os.signpost - -fileprivate extension OSLog { - static let workflow = OSLog(subsystem: "com.squareup.Workflow", category: "Workflow") - static let worker = OSLog(subsystem: "com.squareup.Workflow", category: "Worker") -} - -// MARK: - - -/// Simple class that can be used to create signpost IDs based on an object pointer. -final class SignpostRef { - init() {} -} - -final class WorkflowLogger { - // MARK: Workflows - - static func logWorkflowStarted(ref: WorkflowNode) { - if #available(iOS 12.0, macOS 10.14, *) { - let signpostID = OSSignpostID(log: .workflow, object: ref) - os_signpost(.begin, log: .workflow, name: "Alive", signpostID: signpostID, - "Workflow: %{public}@", String(describing: WorkflowType.self)) - } - } - - static func logWorkflowFinished(ref: WorkflowNode) { - if #available(iOS 12.0, macOS 10.14, *) { - let signpostID = OSSignpostID(log: .workflow, object: ref) - os_signpost(.end, log: .workflow, name: "Alive", signpostID: signpostID) - } - } - - static func logSinkEvent(ref: AnyObject, action: Action) { - if #available(iOS 12.0, macOS 10.14, *) { - let signpostID = OSSignpostID(log: .workflow, object: ref) - os_signpost(.event, log: .workflow, name: "Sink Event", signpostID: signpostID, - "Event for workflow: %{public}@", String(describing: Action.WorkflowType.self)) - } - } - - // MARK: Rendering - - static func logWorkflowStartedRendering(ref: WorkflowNode) { - if #available(iOS 12.0, macOS 10.14, *) { - let signpostID = OSSignpostID(log: .workflow, object: ref) - os_signpost(.begin, log: .workflow, name: "Render", signpostID: signpostID, - "Render Workflow: %{public}@", String(describing: WorkflowType.self)) - } - } - - static func logWorkflowFinishedRendering(ref: WorkflowNode) { - if #available(iOS 12.0, macOS 10.14, *) { - let signpostID = OSSignpostID(log: .workflow, object: ref) - os_signpost(.end, log: .workflow, name: "Render", signpostID: signpostID) - } - } - - // MARK: - Workers - - static func logWorkerStartedRunning(ref: AnyObject, workerType: WorkerType.Type) { - if #available(iOS 12.0, macOS 10.14, *) { - let signpostID = OSSignpostID(log: .worker, object: ref) - os_signpost(.begin, log: .worker, name: "Running", signpostID: signpostID, - "Worker: %{public}@", String(describing: WorkerType.self)) - } - } - - static func logWorkerFinishedRunning(ref: AnyObject, status: StaticString) { - if #available(iOS 12.0, macOS 10.14, *) { - let signpostID = OSSignpostID(log: .worker, object: ref) - os_signpost(.end, log: .worker, name: "Running", signpostID: signpostID, status) - } - } - - static func logWorkerOutput(ref: AnyObject, workerType: WorkerType.Type) { - if #available(iOS 12.0, macOS 10.14, *) { - let signpostID = OSSignpostID(log: .worker, object: ref) - os_signpost(.event, log: .worker, name: "Worker Event", signpostID: signpostID, - "Event: %{public}@", String(describing: WorkerType.self)) - } - } -} diff --git a/swift/Workflow/Sources/WorkflowNode.swift b/swift/Workflow/Sources/WorkflowNode.swift deleted file mode 100644 index 408df8f85..000000000 --- a/swift/Workflow/Sources/WorkflowNode.swift +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 ReactiveSwift - -/// Manages a running workflow. -final class WorkflowNode { - /// Holds the current state of the workflow - private var state: WorkflowType.State - - /// Holds the current workflow. - private var workflow: WorkflowType - - var onOutput: ((Output) -> Void)? - - /// Manages the children of this workflow, including diffs during/after render passes. - private let subtreeManager = SubtreeManager() - - init(workflow: WorkflowType) { - /// Get the initial state - self.workflow = workflow - self.state = workflow.makeInitialState() - - WorkflowLogger.logWorkflowStarted(ref: self) - - subtreeManager.onUpdate = { [weak self] output in - self?.handle(subtreeOutput: output) - } - } - - deinit { - WorkflowLogger.logWorkflowFinished(ref: self) - } - - /// Handles an event produced by the subtree manager - private func handle(subtreeOutput: SubtreeManager.Output) { - let output: Output - - switch subtreeOutput { - case let .update(event, source): - /// Apply the update to the current state - let outputEvent = event.apply(toState: &state) - - /// Finally, we tell the outside world that our state has changed (including an output event if it exists). - output = Output( - outputEvent: outputEvent, - debugInfo: WorkflowUpdateDebugInfo( - workflowType: "\(WorkflowType.self)", - kind: .didUpdate(source: source) - ) - ) - - case let .childDidUpdate(debugInfo): - output = Output( - outputEvent: nil, - debugInfo: WorkflowUpdateDebugInfo( - workflowType: "\(WorkflowType.self)", - kind: .childDidUpdate(debugInfo) - ) - ) - } - - onOutput?(output) - } - - func render() -> WorkflowType.Rendering { - WorkflowLogger.logWorkflowStartedRendering(ref: self) - - defer { - WorkflowLogger.logWorkflowFinishedRendering(ref: self) - } - - return subtreeManager.render { context in - workflow - .render( - state: state, - context: context - ) - } - } - - func enableEvents() { - subtreeManager.enableEvents() - } - - /// Updates the workflow. - func update(workflow: WorkflowType) { - workflow.workflowDidChange(from: self.workflow, state: &state) - self.workflow = workflow - } - - func makeDebugSnapshot() -> WorkflowHierarchyDebugSnapshot { - return WorkflowHierarchyDebugSnapshot( - workflowType: "\(WorkflowType.self)", - stateDescription: "\(state)", - children: subtreeManager.makeDebugSnapshot() - ) - } -} - -extension WorkflowNode { - struct Output { - var outputEvent: WorkflowType.Output? - var debugInfo: WorkflowUpdateDebugInfo - } -} diff --git a/swift/Workflow/Tests/AnyWorkflowTests.swift b/swift/Workflow/Tests/AnyWorkflowTests.swift deleted file mode 100644 index 3eaa12bc5..000000000 --- a/swift/Workflow/Tests/AnyWorkflowTests.swift +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 ReactiveSwift -import XCTest -@testable import Workflow - -public class AnyWorkflowTests: XCTestCase { - func testRendersWrappedWorkflow() { - let workflow = AnyWorkflow(SimpleWorkflow(string: "asdf")) - let node = WorkflowNode(workflow: PassthroughWorkflow(child: workflow)) - - XCTAssertEqual(node.render(), "fdsa") - } - - func testMapRendering() { - let workflow = SimpleWorkflow(string: "asdf") - .mapRendering { string -> String in - string + "dsa" - } - let node = WorkflowNode(workflow: PassthroughWorkflow(child: workflow)) - - XCTAssertEqual(node.render(), "fdsadsa") - } -} - -/// Has no state or output, simply renders a reversed string -private struct PassthroughWorkflow: Workflow { - var child: AnyWorkflow -} - -extension PassthroughWorkflow { - struct State {} - - func makeInitialState() -> State { - return State() - } - - func render(state: State, context: RenderContext>) -> Rendering { - return child.rendered(with: context) - } -} - -/// Has no state or output, simply renders a reversed string -private struct SimpleWorkflow: Workflow { - var string: String -} - -extension SimpleWorkflow { - struct State {} - - func makeInitialState() -> State { - return State() - } - - func render(state: State, context: RenderContext) -> String { - return String(string.reversed()) - } -} diff --git a/swift/Workflow/Tests/ConcurrencyTests.swift b/swift/Workflow/Tests/ConcurrencyTests.swift deleted file mode 100644 index fb820cd0e..000000000 --- a/swift/Workflow/Tests/ConcurrencyTests.swift +++ /dev/null @@ -1,648 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 XCTest -@testable import Workflow - -import ReactiveSwift - -final class ConcurrencyTests: XCTestCase { - // Applying an action from a sink must synchronously update the rendering. - func test_sinkRenderLoopIsSynchronous() { - let host = WorkflowHost(workflow: TestWorkflow()) - - let expectation = XCTestExpectation() - var first = true - var observedScreen: TestScreen? - - let disposable = host.rendering.signal.observeValues { rendering in - if first { - expectation.fulfill() - first = false - } - observedScreen = rendering - } - - let initialScreen = host.rendering.value - XCTAssertEqual(0, initialScreen.count) - initialScreen.update() - - // This update happens immediately as a new rendering is generated synchronously. - XCTAssertEqual(1, host.rendering.value.count) - - wait(for: [expectation], timeout: 1.0) - guard let screen = observedScreen else { - XCTFail("Screen was not updated.") - disposable?.dispose() - return - } - XCTAssertEqual(1, screen.count) - - disposable?.dispose() - } - - // Events emitted between `render` on a workflow and `enableEvents` are queued and will be delivered immediately when `enableEvents` is called. - func test_queuedEvents() { - let host = WorkflowHost(workflow: TestWorkflow()) - - let expectation = XCTestExpectation() - var first = true - - let disposable = host.rendering.signal.observeValues { rendering in - if first { - expectation.fulfill() - first = false - // Emit an event when the rendering is first received. - rendering.update() - } - } - - let initialScreen = host.rendering.value - XCTAssertEqual(0, initialScreen.count) - - // Updating the screen will cause two events - the `update` here, and the update caused by the first time the rendering changes. - initialScreen.update() - - XCTAssertEqual(2, host.rendering.value.count) - - wait(for: [expectation], timeout: 1.0) - - disposable?.dispose() - } - - // A `sink` is invalidated after a single action has been received. However, if the next `render` pass uses a sink - // of the same type, actions sent to an old sink should be proxied through the new sink. - // This allows for a UI that does not synchronously update to use the new sink. - func test_old_sink_proxies_to_new_sink() { - let host = WorkflowHost(workflow: TestWorkflow()) - - // Capture the initial screen and corresponding closure that uses the original sink. - let initialScreen = host.rendering.value - XCTAssertEqual(0, initialScreen.count) - - // Send an action to the workflow. This invalidates this sink, but the next render pass declares a - // sink of the same type. - initialScreen.update() - - let secondScreen = host.rendering.value - XCTAssertEqual(1, secondScreen.count) - - // Send an action from the original screen and sink. It should be proxied through the most recent sink. - initialScreen.update() - - let thirdScreen = host.rendering.value - XCTAssertEqual(2, thirdScreen.count) - } - - // If a previous `sink` has been invalidated due to receiving an action, and a new sink of the same type - // is not redeclared on the subsequent render pass, it should be considered invalid and not allowed to send actions. - func test_invalidate_old_sink_if_not_redeclared() { - let host = WorkflowHost(workflow: OneShotWorkflow()) - - // Capture the initial screen and corresponding closure that uses the original sink. - let initialScreen = host.rendering.value - XCTAssertEqual(0, initialScreen.count) - - // Send an action to the workflow. This invalidates this sink, but the next render pass declares a - // sink of the same type. - initialScreen.update() - - let secondScreen = host.rendering.value - XCTAssertEqual(1, secondScreen.count) - - // MANUAL TEST CASE: Uncomment to validate this fatal errors. - // Calling `update` uses the original sink. This will fail with a fatalError as the sink was not redeclared. - // initialScreen.update() - - // If the sink *was* still valid, this would be correct. However, it should just fail and be `1` still. - // XCTAssertEqual(2, secondScreen.count) - // Actual expected result, if we had not fatal errored. - XCTAssertEqual(1, host.rendering.value.count) - - struct OneShotWorkflow: Workflow { - typealias Output = Never - struct State { - var count: Int - } - - func makeInitialState() -> State { - return State(count: 0) - } - - enum Action: WorkflowAction { - typealias WorkflowType = OneShotWorkflow - - case updated - - func apply(toState state: inout State) -> Never? { - switch self { - case .updated: - state.count += 1 - return nil - } - } - } - - typealias Rendering = TestScreen - func render(state: State, context: RenderContext) -> Rendering { - let update: () -> Void - if state.count == 0 { - let sink = context.makeSink(of: Action.self) - update = { - sink.send(.updated) - } - } else { - update = {} - } - return TestScreen(count: state.count, update: update) - } - } - } - - // When events are queued, the debug info must be received in the order the events were processed. - // This is to validate that `enableEvents` is tail recursive when handled by the WorkflowHost. - func test_debugEventsAreOrdered() { - final class Debugger: WorkflowDebugger { - var snapshots: [WorkflowHierarchyDebugSnapshot] = [] - - func didEnterInitialState(snapshot: WorkflowHierarchyDebugSnapshot) { - // nop - } - - func didUpdate(snapshot: WorkflowHierarchyDebugSnapshot, updateInfo: WorkflowUpdateDebugInfo) { - snapshots.append(snapshot) - } - } - - let debugger = Debugger() - let host = WorkflowHost(workflow: TestWorkflow(), debugger: debugger) - - var first = true - let disposable = host.rendering.signal.observeValues { rendering in - if first { - first = false - rendering.update() - } - } - - let initialScreen = host.rendering.value - initialScreen.update() - - XCTAssertEqual(2, debugger.snapshots.count) - XCTAssertEqual("1", debugger.snapshots[0].stateDescription) - XCTAssertEqual("2", debugger.snapshots[1].stateDescription) - - disposable?.dispose() - } - - func test_childWorkflowsAreSynchronous() { - let host = WorkflowHost(workflow: ParentWorkflow()) - - let initialScreen = host.rendering.value - XCTAssertEqual(0, initialScreen.count) - initialScreen.update() - - // This update happens immediately as a new rendering is generated synchronously. - // Both the child updates from the action (incrementing state by 1) as well as the - // parent from the output (incrementing its state by 10) - XCTAssertEqual(11, host.rendering.value.count) - - struct ParentWorkflow: Workflow { - struct State { - var count: Int - } - - func makeInitialState() -> State { - return State(count: 0) - } - - enum Action: WorkflowAction { - typealias WorkflowType = ParentWorkflow - - case update - - func apply(toState state: inout State) -> Output? { - switch self { - case .update: - state.count += 10 - return nil - } - } - } - - typealias Rendering = TestScreen - - func render(state: State, context: RenderContext) -> Rendering { - var childScreen = TestWorkflow(running: .idle, signal: TestSignal()) - .mapOutput { output -> Action in - switch output { - case .emit: - return .update - } - } - .rendered(with: context) - - childScreen.count += state.count - return childScreen - } - } - } - - // Signals are subscribed on a different scheduler than the UI scheduler, - // which means that if they fire immediately, the action will be received after - // `render` has completed. - func test_subscriptionsAreAsync() { - let signal = TestSignal() - let host = WorkflowHost( - workflow: TestWorkflow( - running: .signal, - signal: signal - )) - - let expectation = XCTestExpectation() - let disposable = host.rendering.signal.observeValues { rendering in - expectation.fulfill() - } - - let screen = host.rendering.value - - XCTAssertEqual(0, screen.count) - - signal.send(value: 1) - - XCTAssertEqual(0, host.rendering.value.count) - - wait(for: [expectation], timeout: 1.0) - - XCTAssertEqual(1, host.rendering.value.count) - - disposable?.dispose() - } - - func test_allSubscriptionActionsAreApplied() { - let signal1 = TestSignal() - let signal2 = TestSignal() - let host = WorkflowHost( - workflow: TestWorkflow( - running: .doubleSubscribing(secondSignal: signal2), - signal: signal1 - ) - ) - - let renderingExpectation = XCTestExpectation() - let outputExpectation = XCTestExpectation() - let outDisposable = host.output.signal.observeValues { output in - outputExpectation.fulfill() - } - - let disposable = host.rendering.signal.observeValues { rendering in - renderingExpectation.fulfill() - } - - let screen = host.rendering.value - - XCTAssertEqual(0, screen.count) - - signal1.send(value: 1) - signal2.send(value: 2) - - XCTAssertEqual(0, host.rendering.value.count) - - wait(for: [renderingExpectation, outputExpectation], timeout: 1.0) - - XCTAssertEqual(101, host.rendering.value.count) - - disposable?.dispose() - outDisposable?.dispose() - } - - // Workers are subscribed on a different scheduler than the UI scheduler, - // which means that if they fire immediately, the action will be received after - // `render` has completed. - func test_workersAreAsync() { - let host = WorkflowHost( - workflow: TestWorkflow( - running: .worker)) - - let expectation = XCTestExpectation() - let disposable = host.rendering.signal.observeValues { rendering in - expectation.fulfill() - } - - XCTAssertEqual(0, host.rendering.value.count) - - wait(for: [expectation], timeout: 1.0) - XCTAssertEqual(1, host.rendering.value.count) - - disposable?.dispose() - } - - // Since event pipes are reused for the same type, validate that the `AnyWorkflowAction` - // defined event pipes still sends through the correct action. - // Because they are just backed by type, not the actual action, they send the actions appropriately. - // (Thus, there is a single backing `TypedSink` for `AnyWorkflowAction`, but the correct action is applied. - func test_multipleAnyWorkflowAction_sinksDontOverrideEachOther() { - let host = WorkflowHost(workflow: AnyActionWorkflow()) - - let initialScreen = host.rendering.value - XCTAssertEqual(0, initialScreen.count) - - // Update using the first action. - initialScreen.updateFirst() - - let secondScreen = host.rendering.value - XCTAssertEqual(1, secondScreen.count) - - // Update using the second action. - secondScreen.updateSecond() - XCTAssertEqual(11, host.rendering.value.count) - - struct AnyActionWorkflow: Workflow { - enum Output { - case emit - } - - struct State { - var count: Int - } - - func makeInitialState() -> State { - return State(count: 0) - } - - enum FirstAction: WorkflowAction { - typealias WorkflowType = AnyActionWorkflow - case update - - func apply(toState state: inout State) -> Output? { - switch self { - case .update: - state.count += 1 - } - return nil - } - } - - enum SecondAction: WorkflowAction { - typealias WorkflowType = AnyActionWorkflow - case update - - func apply(toState state: inout State) -> Output? { - switch self { - case .update: - state.count += 10 - } - return nil - } - } - - struct TestScreen { - var count: Int - var updateFirst: () -> Void - var updateSecond: () -> Void - } - - typealias Rendering = TestScreen - - func render(state: State, context: RenderContext) -> Rendering { - let firstSink = context - .makeSink( - of: AnyWorkflowAction.self) - .contraMap { (action: FirstAction) -> AnyWorkflowAction in - AnyWorkflowAction(action) - } - - let secondSink = context - .makeSink( - of: AnyWorkflowAction.self) - .contraMap { (action: SecondAction) -> AnyWorkflowAction in - AnyWorkflowAction(action) - } - - return TestScreen( - count: state.count, - updateFirst: { - firstSink.send(.update) - }, - updateSecond: { - secondSink.send(.update) - } - ) - } - } - } - - /// Since event pipes are allowed to be reused, and shared for the same backing action type - /// validate that they are also only reused for the same source type - ie: a sink for an - /// action should not use the same event pipe as a worker that maps to the same action type. - /// This will likely need to be a test that will be "correct" when it fatal errors - /// since the behavior would be reusing the wrong event pipe for an old sink that was not - /// redeclared. - func test_eventPipesAreOnlyReusedForSameSource() { - let host = WorkflowHost(workflow: SourceDifferentiatingWorkflow(step: .first)) - - // let initialScreen = host.rendering.value - XCTAssertEqual(0, host.rendering.value.count) - - // Update to the second "step", which will cause a render update, with the sink not being declared. - host.update(workflow: SourceDifferentiatingWorkflow(step: .second)) - // The state should be the same, even though it rendered again. - XCTAssertEqual(0, host.rendering.value.count) - - // MANUAL TEST CASE - // This will fail, as the sink held by `initialScreen` has not be redeclared, even though the backing action is the same for the worker. - // Uncomment to validate this test fails with a fatal error. - // initialScreen.update() - XCTAssertEqual(0, host.rendering.value.count) - - struct SourceDifferentiatingWorkflow: Workflow { - typealias Output = Never - - var step: Step - enum Step { - case first - case second - } - - struct State { - var count: Int - } - - func makeInitialState() -> State { - return State(count: 0) - } - - enum Action: WorkflowAction { - typealias WorkflowType = SourceDifferentiatingWorkflow - - case update - - func apply(toState state: inout State) -> Never? { - switch self { - case .update: - state.count += 1 - return nil - } - } - } - - struct DelayWorker: Worker { - typealias Output = Action - - func run() -> SignalProducer { - return SignalProducer(value: .update).delay(0.1, on: QueueScheduler.main) - } - - func isEquivalent(to otherWorker: DelayWorker) -> Bool { - return true - } - } - - typealias Rendering = TestScreen - - func render(state: State, context: RenderContext) -> Rendering { - let update: () -> Void - switch step { - case .first: - let sink = context.makeSink(of: Action.self) - update = { sink.send(.update) } - - case .second: - update = {} - } - - context.awaitResult(for: DelayWorker(), outputMap: { $0 }) - - return TestScreen(count: state.count, update: update) - } - } - } - - // MARK: - Test Types - - fileprivate class TestSignal { - let (signal, observer) = Signal.pipe() - var sent: Bool = false - - func send(value: Int) { - if !sent { - observer.send(value: value) - sent = true - } - } - } - - fileprivate struct TestScreen { - var count: Int - var update: () -> Void - } - - fileprivate struct TestWorkflow: Workflow { - enum Output { - case emit - } - - init(running: Running = .idle, signal: TestSignal = TestSignal()) { - self.running = running - self.signal = signal - } - - var running: Running - enum Running { - case idle - case signal - case doubleSubscribing(secondSignal: TestSignal) - case worker - } - - var signal: TestSignal - - struct State: CustomStringConvertible { - var count: Int - var running: Running - var signal: TestSignal - - var description: String { - return "\(count)" - } - } - - func makeInitialState() -> State { - return State(count: 0, running: running, signal: signal) - } - - enum Action: WorkflowAction { - typealias WorkflowType = TestWorkflow - - case update - case secondUpdate - - func apply(toState state: inout State) -> Output? { - switch self { - case .update: - state.count += 1 - return .emit - case .secondUpdate: - state.count += 100 - return nil - } - } - } - - typealias Rendering = TestScreen - - func render(state: State, context: RenderContext) -> Rendering { - switch state.running { - case .idle: - break - case .signal: - context.awaitResult(for: signal.signal.asWorker(key: "signal1")) { _ -> Action in - .update - } - - case let .doubleSubscribing(secondSignal: signal2): - context.awaitResult(for: signal2.signal.asWorker(key: "signal2")) { _ -> Action in - .secondUpdate - } - context.awaitResult(for: signal.signal.asWorker(key: "signal1")) { _ -> Action in - .update - } - - case .worker: - context.awaitResult(for: TestWorker()) - } - - let sink = context.makeSink(of: Action.self) - - return TestScreen( - count: state.count, - update: { sink.send(.update) } - ) - } - - struct TestWorker: Worker { - typealias Output = TestWorkflow.Action - - func run() -> SignalProducer { - return SignalProducer(value: .update) - } - - func isEquivalent(to otherWorker: TestWorker) -> Bool { - return true - } - } - } -} diff --git a/swift/Workflow/Tests/DebuggingTests.swift b/swift/Workflow/Tests/DebuggingTests.swift deleted file mode 100644 index f234d741e..000000000 --- a/swift/Workflow/Tests/DebuggingTests.swift +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 XCTest -@testable import Workflow - -public class DebuggingTests: XCTestCase { - func test_debugTreeCoding() { - let tree = WorkflowHierarchyDebugSnapshot( - workflowType: "foo", - stateDescription: "bar", - children: [ - WorkflowHierarchyDebugSnapshot.Child( - key: "a", - snapshot: WorkflowHierarchyDebugSnapshot( - workflowType: "hello", - stateDescription: "world" - ) - ), - WorkflowHierarchyDebugSnapshot.Child( - key: "b", - snapshot: WorkflowHierarchyDebugSnapshot( - workflowType: "testing", - stateDescription: "123" - ) - ), - ] - ) - - let encoded = try! JSONEncoder().encode(tree) - let decoded = try! JSONDecoder().decode(WorkflowHierarchyDebugSnapshot.self, from: encoded) - - XCTAssertEqual(tree, decoded) - } - - func test_debugUpdateInfoCoding() { - let info = WorkflowUpdateDebugInfo( - workflowType: "foo", - kind: .didUpdate( - source: .subtree( - WorkflowUpdateDebugInfo( - workflowType: "baz", - kind: .didUpdate(source: .external) - ) - )) - ) - - let encoded = try! JSONEncoder().encode(info) - let decoded = try! JSONDecoder().decode(WorkflowUpdateDebugInfo.self, from: encoded) - - XCTAssertEqual(info, decoded) - } -} diff --git a/swift/Workflow/Tests/SubtreeManagerTests.swift b/swift/Workflow/Tests/SubtreeManagerTests.swift deleted file mode 100644 index 5661b9e8a..000000000 --- a/swift/Workflow/Tests/SubtreeManagerTests.swift +++ /dev/null @@ -1,466 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 ReactiveSwift -import XCTest -@testable import Workflow - -final class SubtreeManagerTests: XCTestCase { - func test_maintainsChildrenBetweenRenderPasses() { - let manager = WorkflowNode.SubtreeManager() - XCTAssertTrue(manager.childWorkflows.isEmpty) - - _ = manager.render { context -> TestViewModel in - context.render( - workflow: TestWorkflow(), - key: "", - outputMap: { _ in AnyWorkflowAction.noAction } - ) - } - - XCTAssertEqual(manager.childWorkflows.count, 1) - let child = manager.childWorkflows.values.first! - - _ = manager.render { context -> TestViewModel in - context.render( - workflow: TestWorkflow(), - key: "", - outputMap: { _ in AnyWorkflowAction.noAction } - ) - } - - XCTAssertEqual(manager.childWorkflows.count, 1) - XCTAssertTrue(manager.childWorkflows.values.first === child) - } - - func test_removesUnusedChildrenAfterRenderPasses() { - let manager = WorkflowNode.SubtreeManager() - _ = manager.render { context -> TestViewModel in - context.render( - workflow: TestWorkflow(), - key: "", - outputMap: { _ in AnyWorkflowAction.noAction } - ) - } - - XCTAssertEqual(manager.childWorkflows.count, 1) - - _ = manager.render { context -> Void in - } - - XCTAssertTrue(manager.childWorkflows.isEmpty) - } - - func test_emitsChildEvents() { - let manager = WorkflowNode.SubtreeManager() - - var events: [AnyWorkflowAction] = [] - - manager.onUpdate = { - switch $0 { - case let .update(event, _): - events.append(event) - default: - break - } - } - - let viewModel = manager.render { context -> TestViewModel in - context.render( - workflow: TestWorkflow(), - key: "", - outputMap: { _ in AnyWorkflowAction.noAction } - ) - } - manager.enableEvents() - - viewModel.onTap() - viewModel.onTap() - - RunLoop.current.run(until: Date().addingTimeInterval(0.1)) - - XCTAssertEqual(events.count, 2) - } - - func test_emitsChangeEvents() { - let manager = WorkflowNode.SubtreeManager() - - var changeCount = 0 - - manager.onUpdate = { _ in - changeCount += 1 - } - - let viewModel = manager.render { context -> TestViewModel in - context.render( - workflow: TestWorkflow(), - key: "", - outputMap: { _ in AnyWorkflowAction.noAction } - ) - } - manager.enableEvents() - - viewModel.onToggle() - viewModel.onToggle() - - RunLoop.current.run(until: Date().addingTimeInterval(0.1)) - - XCTAssertEqual(changeCount, 2) - } - - func test_invalidatesContextAfterRender() { - let manager = WorkflowNode.SubtreeManager() - - var escapingContext: RenderContext! - - _ = manager.render { context -> TestViewModel in - XCTAssertTrue(context.isValid) - escapingContext = context - return context.render( - workflow: TestWorkflow(), - key: "", - outputMap: { _ in AnyWorkflowAction.noAction } - ) - } - manager.enableEvents() - - XCTAssertFalse(escapingContext.isValid) - } - - // A worker declared on a first `render` pass that is not on a subsequent should have the work cancelled. - func test_cancelsWorkers() { - struct WorkerWorkflow: Workflow { - var startExpectation: XCTestExpectation - var endExpectation: XCTestExpectation - - enum State { - case notWorking - case working - } - - func makeInitialState() -> WorkerWorkflow.State { - return .notWorking - } - - func render(state: WorkerWorkflow.State, context: RenderContext) -> Bool { - switch state { - case .notWorking: - return false - case .working: - context.awaitResult( - for: ExpectingWorker( - startExpectation: startExpectation, - endExpectation: endExpectation - ), - outputMap: { output -> AnyWorkflowAction in - AnyWorkflowAction.noAction - } - ) - return true - } - } - - struct ExpectingWorker: Worker { - var startExpectation: XCTestExpectation - var endExpectation: XCTestExpectation - - typealias Output = Void - - func run() -> SignalProducer { - return SignalProducer({ [weak startExpectation, weak endExpectation] observer, lifetime in - lifetime.observeEnded { - endExpectation?.fulfill() - } - - startExpectation?.fulfill() - }) - } - - func isEquivalent(to otherWorker: WorkerWorkflow.ExpectingWorker) -> Bool { - return true - } - } - } - - let startExpectation = XCTestExpectation() - let endExpectation = XCTestExpectation() - let manager = WorkflowNode.SubtreeManager() - - let isRunning = manager.render { context -> Bool in - WorkerWorkflow( - startExpectation: startExpectation, - endExpectation: endExpectation - ) - .render( - state: .working, - context: context - ) - } - - XCTAssertEqual(true, isRunning) - wait(for: [startExpectation], timeout: 1.0) - - let isStillRunning = manager.render { context -> Bool in - WorkerWorkflow( - startExpectation: startExpectation, - endExpectation: endExpectation - ) - .render( - state: .notWorking, - context: context - ) - } - - XCTAssertFalse(isStillRunning) - wait(for: [endExpectation], timeout: 1.0) - } - - func test_subscriptionsUnsubscribe() { - struct SubscribingWorkflow: Workflow { - var signal: Signal? - - struct State {} - - func makeInitialState() -> SubscribingWorkflow.State { - return State() - } - - func render(state: SubscribingWorkflow.State, context: RenderContext) -> Bool { - if let signal = signal { - context.awaitResult(for: signal.asWorker(key: "signal")) { _ -> AnyWorkflowAction in - AnyWorkflowAction.noAction - } - return true - } else { - return false - } - } - } - - let emittedExpectation = XCTestExpectation() - let notEmittedExpectation = XCTestExpectation() - notEmittedExpectation.isInverted = true - - let manager = WorkflowNode.SubtreeManager() - manager.onUpdate = { output in - emittedExpectation.fulfill() - } - - let (signal, observer) = Signal.pipe() - - let isSubscribing = manager.render { context -> Bool in - SubscribingWorkflow( - signal: signal - ) - .render( - state: SubscribingWorkflow.State(), - context: context - ) - } - manager.enableEvents() - - XCTAssertTrue(isSubscribing) - observer.send(value: ()) - wait(for: [emittedExpectation], timeout: 1.0) - - manager.onUpdate = { output in - notEmittedExpectation.fulfill() - } - - let isStillSubscribing = manager.render { context -> Bool in - SubscribingWorkflow( - signal: nil - ) - .render( - state: SubscribingWorkflow.State(), - context: context - ) - } - manager.enableEvents() - - XCTAssertFalse(isStillSubscribing) - - observer.send(value: ()) - wait(for: [notEmittedExpectation], timeout: 1.0) - } - - // MARK: - SideEffect - - func test_maintainsSideEffectLifetimeBetweenRenderPasses() { - let manager = WorkflowNode.SubtreeManager() - XCTAssertTrue(manager.sideEffectLifetimes.isEmpty) - - _ = manager.render { context -> TestViewModel in - context.runSideEffect(key: "helloWorld") { _ in } - return context.render( - workflow: TestWorkflow(), - key: "", - outputMap: { _ in AnyWorkflowAction.noAction } - ) - } - - XCTAssertEqual(manager.sideEffectLifetimes.count, 1) - let sideEffectKey = manager.sideEffectLifetimes.values.first! - - _ = manager.render { context -> TestViewModel in - context.runSideEffect(key: "helloWorld") { _ in - XCTFail("Unexpected SideEffect execution") - } - return context.render( - workflow: TestWorkflow(), - key: "", - outputMap: { _ in AnyWorkflowAction.noAction } - ) - } - - XCTAssertEqual(manager.sideEffectLifetimes.count, 1) - XCTAssertTrue(manager.sideEffectLifetimes.values.first === sideEffectKey) - } - - func test_endsUnusedSideEffectLifetimeAfterRenderPasses() { - let manager = WorkflowNode.SubtreeManager() - XCTAssertTrue(manager.sideEffectLifetimes.isEmpty) - - let lifetimeEndedExpectation = expectation(description: "Lifetime Ended Expectations") - _ = manager.render { context -> TestViewModel in - context.runSideEffect(key: "helloWorld") { lifetime in - lifetime.onEnded { - // Capturing `lifetime` to make sure a retain-cycle will still trigger the `onEnded` block - print("\(lifetime)") - lifetimeEndedExpectation.fulfill() - } - } - return context.render( - workflow: TestWorkflow(), - key: "", - outputMap: { _ in AnyWorkflowAction.noAction } - ) - } - - XCTAssertEqual(manager.sideEffectLifetimes.count, 1) - - _ = manager.render { context -> TestViewModel in - context.render( - workflow: TestWorkflow(), - key: "", - outputMap: { _ in AnyWorkflowAction.noAction } - ) - } - - XCTAssertEqual(manager.sideEffectLifetimes.count, 0) - wait(for: [lifetimeEndedExpectation], timeout: 1) - } - - func test_verifySideEffectsWithDifferentKeysAreExecuted() { - let manager = WorkflowNode.SubtreeManager() - XCTAssertTrue(manager.sideEffectLifetimes.isEmpty) - - let firstSideEffectExecutedExpectation = expectation(description: "FirstSideEffect") - _ = manager.render { context -> TestViewModel in - context.runSideEffect(key: "key-1") { _ in - firstSideEffectExecutedExpectation.fulfill() - } - return context.render( - workflow: TestWorkflow(), - key: "", - outputMap: { _ in AnyWorkflowAction.noAction } - ) - } - - wait(for: [firstSideEffectExecutedExpectation], timeout: 1) - XCTAssertEqual(manager.sideEffectLifetimes.count, 1) - XCTAssertEqual(manager.sideEffectLifetimes.keys.first, "key-1") - - let secondSideEffectExecutedExpectation = expectation(description: "SecondSideEffect") - _ = manager.render { context -> TestViewModel in - context.runSideEffect(key: "key-2") { _ in - secondSideEffectExecutedExpectation.fulfill() - } - return context.render( - workflow: TestWorkflow(), - key: "", - outputMap: { _ in AnyWorkflowAction.noAction } - ) - } - - wait(for: [secondSideEffectExecutedExpectation], timeout: 1) - XCTAssertEqual(manager.sideEffectLifetimes.count, 1) - XCTAssertEqual(manager.sideEffectLifetimes.keys.first, "key-2") - } -} - -private struct TestViewModel { - var onTap: () -> Void - var onToggle: () -> Void -} - -private struct ParentWorkflow: Workflow { - struct State {} - typealias Event = TestWorkflow.Output - typealias Output = Never - - func makeInitialState() -> State { - return State() - } - - func render(state: State, context: RenderContext) -> Never { - fatalError() - } -} - -private struct TestWorkflow: Workflow { - enum State { - case foo - case bar - } - - enum Event: WorkflowAction { - typealias WorkflowType = TestWorkflow - - case changeState - case sendOutput - - func apply(toState state: inout TestWorkflow.State) -> TestWorkflow.Output? { - switch self { - case .changeState: - switch state { - case .foo: state = .bar - case .bar: state = .foo - } - return nil - case .sendOutput: - return .helloWorld - } - } - } - - enum Output { - case helloWorld - } - - func makeInitialState() -> State { - return .foo - } - - func render(state: State, context: RenderContext) -> TestViewModel { - let sink = context.makeSink(of: Event.self) - - return TestViewModel( - onTap: { sink.send(.sendOutput) }, - onToggle: { sink.send(.changeState) } - ) - } -} diff --git a/swift/Workflow/Tests/WorkflowHostTests.swift b/swift/Workflow/Tests/WorkflowHostTests.swift deleted file mode 100644 index e527f5a07..000000000 --- a/swift/Workflow/Tests/WorkflowHostTests.swift +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 Workflow -import XCTest - -final class WorkflowHostTests: XCTestCase { - func test_updatedInputCausesRenderPass() { - let host = WorkflowHost(workflow: TestWorkflow(step: .first)) - - XCTAssertEqual(1, host.rendering.value) - - host.update(workflow: TestWorkflow(step: .second)) - - XCTAssertEqual(2, host.rendering.value) - } - - fileprivate struct TestWorkflow: Workflow { - var step: Step - enum Step { - case first - case second - } - - struct State {} - func makeInitialState() -> State { - return State() - } - - typealias Rendering = Int - - func render(state: State, context: RenderContext) -> Rendering { - switch step { - case .first: - return 1 - case .second: - return 2 - } - } - } -} diff --git a/swift/Workflow/Tests/WorkflowNodeTests.swift b/swift/Workflow/Tests/WorkflowNodeTests.swift deleted file mode 100644 index efb893615..000000000 --- a/swift/Workflow/Tests/WorkflowNodeTests.swift +++ /dev/null @@ -1,428 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 XCTest -@testable import Workflow - -import ReactiveSwift - -final class WorkflowNodeTests: XCTestCase { - func test_rendersSimpleWorkflow() { - let node = WorkflowNode(workflow: SimpleWorkflow(string: "Foo")) - XCTAssertEqual(node.render(), "ooF") - } - - func test_rendersNestedWorkflows() { - let node = WorkflowNode( - workflow: CompositeWorkflow( - a: SimpleWorkflow(string: "Hello"), - b: SimpleWorkflow(string: "World") - )) - - XCTAssertEqual(node.render().aRendering, "olleH") - XCTAssertEqual(node.render().bRendering, "dlroW") - } - - func test_childWorkflowsEmitOutputEvents() { - typealias WorkflowType = CompositeWorkflow - - let workflow = CompositeWorkflow( - a: EventEmittingWorkflow(string: "Hello"), - b: SimpleWorkflow(string: "World") - ) - - let node = WorkflowNode(workflow: workflow) - - let rendering = node.render() - node.enableEvents() - - var outputs: [WorkflowType.Output] = [] - - let expectation = XCTestExpectation(description: "Node output") - - node.onOutput = { value in - if let output = value.outputEvent { - outputs.append(output) - expectation.fulfill() - } - } - - rendering.aRendering.someoneTappedTheButton() - - wait(for: [expectation], timeout: 1.0) - - XCTAssertEqual(outputs, [WorkflowType.Output.childADidSomething(.helloWorld)]) - } - - func test_childWorkflowsEmitStateChangeEvents() { - typealias WorkflowType = CompositeWorkflow - - let workflow = CompositeWorkflow( - a: StateTransitioningWorkflow(), - b: SimpleWorkflow(string: "World") - ) - - let node = WorkflowNode(workflow: workflow) - - let expectation = XCTestExpectation(description: "State Change") - var stateChangeCount = 0 - - node.onOutput = { _ in - stateChangeCount += 1 - if stateChangeCount == 3 { - expectation.fulfill() - } - } - - var aRendering = node.render().aRendering - node.enableEvents() - aRendering.toggle() - - aRendering = node.render().aRendering - node.enableEvents() - aRendering.toggle() - - aRendering = node.render().aRendering - node.enableEvents() - aRendering.toggle() - - wait(for: [expectation], timeout: 1.0) - - XCTAssertEqual(stateChangeCount, 3) - } - - func test_debugUpdateInfo() { - typealias WorkflowType = CompositeWorkflow - - let workflow = CompositeWorkflow( - a: EventEmittingWorkflow(string: "Hello"), - b: SimpleWorkflow(string: "World") - ) - - let node = WorkflowNode(workflow: workflow) - - let rendering = node.render() - node.enableEvents() - - var emittedDebugInfo: [WorkflowUpdateDebugInfo] = [] - - let expectation = XCTestExpectation(description: "Output") - node.onOutput = { value in - emittedDebugInfo.append(value.debugInfo) - expectation.fulfill() - } - - rendering.aRendering.someoneTappedTheButton() - - wait(for: [expectation], timeout: 1.0) - - XCTAssertEqual(emittedDebugInfo.count, 1) - - let debugInfo = emittedDebugInfo[0] - - XCTAssert(debugInfo.workflowType == "\(WorkflowType.self)") - - /// Test the shape of the emitted debug info - switch debugInfo.kind { - case .childDidUpdate: - XCTFail() - case let .didUpdate(source): - switch source { - case .external, .worker, .sideEffect: - XCTFail() - case let .subtree(childInfo): - XCTAssert(childInfo.workflowType == "\(EventEmittingWorkflow.self)") - switch childInfo.kind { - case .childDidUpdate: - XCTFail() - case let .didUpdate(source): - switch source { - case .external: - break - case .subtree(_), .worker, .sideEffect: - XCTFail() - } - } - } - } - } - - func test_debugTreeSnapshots() { - typealias WorkflowType = CompositeWorkflow - - let workflow = CompositeWorkflow( - a: EventEmittingWorkflow(string: "Hello"), - b: SimpleWorkflow(string: "World") - ) - let node = WorkflowNode(workflow: workflow) - _ = node.render() // the debug snapshow always reflects the tree after the latest render pass - - let snapshot = node.makeDebugSnapshot() - - let expectedSnapshot = WorkflowHierarchyDebugSnapshot( - workflowType: "\(WorkflowType.self)", - stateDescription: "\(WorkflowType.State())", - children: [ - WorkflowHierarchyDebugSnapshot.Child( - key: "a", - snapshot: WorkflowHierarchyDebugSnapshot( - workflowType: "\(EventEmittingWorkflow.self)", - stateDescription: "\(EventEmittingWorkflow.State())" - ) - ), - WorkflowHierarchyDebugSnapshot.Child( - key: "b", - snapshot: WorkflowHierarchyDebugSnapshot( - workflowType: "\(SimpleWorkflow.self)", - stateDescription: "\(SimpleWorkflow.State())" - ) - ), - ] - ) - - XCTAssertEqual(snapshot, expectedSnapshot) - } - - func test_handlesRepeatedWorkerOutputs() { - struct WF: Workflow { - struct State {} - - typealias Output = Int - - typealias Rendering = Void - - func makeInitialState() -> WF.State { - return State() - } - - func render(state: WF.State, context: RenderContext) { - context.awaitResult(for: TestWorker()) { output in - AnyWorkflowAction(sendingOutput: output) - } - } - } - - struct TestWorker: Worker { - func isEquivalent(to otherWorker: TestWorker) -> Bool { - return true - } - - func run() -> SignalProducer { - return SignalProducer { observer, lifetime in - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - observer.send(value: 1) - observer.send(value: 2) - observer.sendCompleted() - } - } - } - } - - let expectation = XCTestExpectation(description: "Test Worker") - var outputs: [Int] = [] - - let node = WorkflowNode(workflow: WF()) - node.onOutput = { output in - if let outputInt = output.outputEvent { - outputs.append(outputInt) - - if outputs.count == 2 { - expectation.fulfill() - } - } - } - - node.render() - node.enableEvents() - - wait(for: [expectation], timeout: 1.0) - - XCTAssertEqual(outputs, [1, 2]) - } -} - -/// Renders two child state machines of types `A` and `B`. -private struct CompositeWorkflow: Workflow where - A: Workflow, - B: Workflow { - var a: A - var b: B -} - -extension CompositeWorkflow { - struct State {} - - struct Rendering { - var aRendering: A.Rendering - var bRendering: B.Rendering - } - - enum Output { - case childADidSomething(A.Output) - case childBDidSomething(B.Output) - } - - enum Event: WorkflowAction { - case a(A.Output) - case b(B.Output) - - typealias WorkflowType = CompositeWorkflow - - func apply(toState state: inout CompositeWorkflow.State) -> CompositeWorkflow.Output? { - switch self { - case let .a(childOutput): - return .childADidSomething(childOutput) - case let .b(childOutput): - return .childBDidSomething(childOutput) - } - } - } - - func makeInitialState() -> CompositeWorkflow.State { - return State() - } - - func render(state: State, context: RenderContext>) -> Rendering { - return Rendering( - aRendering: a - .mapOutput { Event.a($0) } - .rendered(with: context, key: "a"), - bRendering: b - .mapOutput { Event.b($0) } - .rendered(with: context, key: "b") - ) - } -} - -extension CompositeWorkflow.Rendering: Equatable where A.Rendering: Equatable, B.Rendering: Equatable { - fileprivate static func == (lhs: CompositeWorkflow.Rendering, rhs: CompositeWorkflow.Rendering) -> Bool { - return lhs.aRendering == rhs.aRendering - && lhs.bRendering == rhs.bRendering - } -} - -extension CompositeWorkflow.Output: Equatable where A.Output: Equatable, B.Output: Equatable { - fileprivate static func == (lhs: CompositeWorkflow.Output, rhs: CompositeWorkflow.Output) -> Bool { - switch (lhs, rhs) { - case let (.childADidSomething(l), .childADidSomething(r)): - return l == r - case let (.childBDidSomething(l), .childBDidSomething(r)): - return l == r - default: - return false - } - } -} - -/// Has no state or output, simply renders a reversed string -private struct SimpleWorkflow: Workflow { - var string: String - - struct State {} - - func makeInitialState() -> State { - return State() - } - - func render(state: State, context: RenderContext) -> String { - return String(string.reversed()) - } -} - -/// Renders to a model that contains a callback, which in turn sends an output event. -private struct EventEmittingWorkflow: Workflow { - var string: String -} - -extension EventEmittingWorkflow { - struct State {} - - struct Rendering { - var someoneTappedTheButton: () -> Void - } - - func makeInitialState() -> State { - return State() - } - - enum Event: Equatable, WorkflowAction { - case tapped - - typealias WorkflowType = EventEmittingWorkflow - - func apply(toState state: inout EventEmittingWorkflow.State) -> EventEmittingWorkflow.Output? { - switch self { - case .tapped: - return .helloWorld - } - } - } - - enum Output: Equatable { - case helloWorld - } - - func render(state: State, context: RenderContext) -> Rendering { - let sink = context.makeSink(of: Event.self) - - return Rendering(someoneTappedTheButton: { sink.send(.tapped) }) - } -} - -/// Renders to a model that contains a callback, which in turn sends an output event. -private struct StateTransitioningWorkflow: Workflow { - typealias State = Bool - - typealias Output = Never - - struct Rendering { - var toggle: () -> Void - var currentValue: Bool - } - - func makeInitialState() -> Bool { - return false - } - - func render(state: State, context: RenderContext) -> Rendering { - let sink = context.makeSink(of: Event.self) - - return Rendering( - toggle: { sink.send(.toggle) }, - currentValue: state - ) - } - - enum Event: WorkflowAction { - case toggle - - typealias WorkflowType = StateTransitioningWorkflow - - func apply(toState state: inout Bool) -> Never? { - switch self { - case .toggle: - state.toggle() - } - return nil - } - } -} - -#if compiler(>=5.0) -// Never gains Equatable and Hashable conformance in Swift 5 -#else - extension Never: Equatable {} -#endif diff --git a/swift/WorkflowSwiftUI/Sources/WorkflowView.swift b/swift/WorkflowSwiftUI/Sources/WorkflowView.swift deleted file mode 100644 index 7afe1a74c..000000000 --- a/swift/WorkflowSwiftUI/Sources/WorkflowView.swift +++ /dev/null @@ -1,298 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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. - */ - -#if canImport(SwiftUI) && canImport(Combine) && swift(>=5.1) - - import Combine - import ReactiveSwift - import SwiftUI - import Workflow - - /// Hosts a Workflow-powered view hierarchy. - /// - /// Example: - /// - /// ``` - /// var body: some View { - /// WorkflowView(workflow: MyWorkflow(), onOutput: { self.handleOutput($0) }) { rendering in - /// VStack { - /// - /// Text("The value is \(rendering.value)") - /// - /// Button(action: rendering.onIncrement) { - /// Text("+") - /// } - /// - /// Button(action: rendering.onDecrement) { - /// Text("-") - /// } - /// - /// } - /// } - /// } - /// ``` - @available(iOS 13.0, macOS 10.15, *) - public struct WorkflowView: View { - /// The workflow implementation to use - public var workflow: T - - /// A handler for any output events emitted by the workflow - public var onOutput: (T.Output) -> Void - - /// A closure that maps the workflow's rendering type into a view of type `Content`. - public var content: (T.Rendering) -> Content - - public init(workflow: T, onOutput: @escaping (T.Output) -> Void, content: @escaping (T.Rendering) -> Content) { - self.onOutput = onOutput - self.content = content - self.workflow = workflow - } - - public var body: some View { - IntermediateView( - workflow: workflow, - onOutput: onOutput, - content: content - ) - } - } - - @available(iOS 13.0, macOS 10.15, *) - extension WorkflowView where T.Output == Never { - /// Convenience initializer for workflows with no output. - public init(workflow: T, content: @escaping (T.Rendering) -> Content) { - self.init(workflow: workflow, onOutput: { _ in }, content: content) - } - } - - @available(iOS 13.0, macOS 10.15, *) - extension WorkflowView where T.Rendering == Content { - /// Convenience initializer for workflows whose rendering type conforms to `View`. - public init(workflow: T, onOutput: @escaping (T.Output) -> Void) { - self.init(workflow: workflow, onOutput: onOutput, content: { $0 }) - } - } - - @available(iOS 13.0, macOS 10.15, *) - extension WorkflowView where T.Output == Never, T.Rendering == Content { - /// Convenience initializer for workflows with no output whose rendering type conforms to `View`. - public init(workflow: T) { - self.init(workflow: workflow, onOutput: { _ in }, content: { $0 }) - } - } - - // We use a `UIViewController/UIViewControllerRepresentable` here to drop back to UIKit because it gives us a predictable - // update mechanism via `updateUIViewController(_:context:)`. If we were to manage a `WorkflowHost` instance directly - // within a SwiftUI view we would need to update the host with the updated workflow from our implementation of `body`. - // Performing work within the body accessor is strongly discouraged, so we jump back into UIKit for a second here. - @available(iOS 13.0, macOS 10.15, *) - fileprivate struct IntermediateView { - var workflow: T - var onOutput: (T.Output) -> Void - var content: (T.Rendering) -> Content - } - - #if canImport(UIKit) - - import UIKit - - @available(iOS 13.0, *) - extension IntermediateView: UIViewControllerRepresentable { - func makeUIViewController(context: UIViewControllerRepresentableContext>) -> WorkflowHostingViewController { - WorkflowHostingViewController(workflow: workflow, content: content) - } - - func updateUIViewController(_ uiViewController: WorkflowHostingViewController, context: UIViewControllerRepresentableContext>) { - uiViewController.content = content - uiViewController.onOutput = onOutput - uiViewController.update(to: workflow) - } - } - - @available(iOS 13.0, *) - fileprivate final class WorkflowHostingViewController: UIViewController { - private let workflowHost: WorkflowHost - private let hostingController: UIHostingController> - private let rootViewProvider: RootViewProvider - - var content: (T.Rendering) -> Content - var onOutput: (T.Output) -> Void - - private let (lifetime, token) = Lifetime.make() - - init(workflow: T, content: @escaping (T.Rendering) -> Content) { - self.content = content - self.onOutput = { _ in } - - self.workflowHost = WorkflowHost(workflow: workflow) - self.rootViewProvider = RootViewProvider(view: content(workflowHost.rendering.value)) - self.hostingController = UIHostingController(rootView: RootView(provider: rootViewProvider)) - - super.init(nibName: nil, bundle: nil) - - addChild(hostingController) - view.addSubview(hostingController.view) - hostingController.didMove(toParent: self) - - workflowHost - .rendering - .signal - .take(during: lifetime) - .observeValues { [weak self] rendering in - self?.didEmit(rendering: rendering) - } - - workflowHost - .output - .take(during: lifetime) - .observeValues { [weak self] output in - self?.didEmit(output: output) - } - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - hostingController.view.frame = view.bounds - } - - private func didEmit(rendering: T.Rendering) { - rootViewProvider.view = content(rendering) - } - - private func didEmit(output: T.Output) { - onOutput(output) - } - - func update(to workflow: T) { - workflowHost.update(workflow: workflow) - } - } - - #elseif canImport(AppKit) - - import AppKit - - @available(OSX 10.15, *) - extension IntermediateView: NSViewControllerRepresentable { - func makeNSViewController(context: NSViewControllerRepresentableContext>) -> WorkflowHostingViewController { - WorkflowHostingViewController(workflow: workflow, content: content) - } - - func updateNSViewController(_ nsViewController: WorkflowHostingViewController, context: NSViewControllerRepresentableContext>) { - nsViewController.content = content - nsViewController.onOutput = onOutput - nsViewController.update(to: workflow) - } - } - - @available(macOS 10.15, *) - fileprivate final class WorkflowHostingViewController: NSViewController { - private let workflowHost: WorkflowHost - private let hostingController: NSHostingController> - private let rootViewProvider: RootViewProvider - - var content: (T.Rendering) -> Content - var onOutput: (T.Output) -> Void - - private let (lifetime, token) = Lifetime.make() - - init(workflow: T, content: @escaping (T.Rendering) -> Content) { - self.content = content - self.onOutput = { _ in } - - self.workflowHost = WorkflowHost(workflow: workflow) - self.rootViewProvider = RootViewProvider(view: content(workflowHost.rendering.value)) - self.hostingController = NSHostingController(rootView: RootView(provider: rootViewProvider)) - - super.init(nibName: nil, bundle: nil) - - addChild(hostingController) - - workflowHost - .rendering - .signal - .take(during: lifetime) - .observeValues { [weak self] rendering in - self?.didEmit(rendering: rendering) - } - - workflowHost - .output - .take(during: lifetime) - .observeValues { [weak self] output in - self?.didEmit(output: output) - } - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func loadView() { - view = NSView() - } - - override func viewDidLoad() { - super.viewDidLoad() - view.addSubview(hostingController.view) - } - - override func viewDidLayout() { - super.viewDidLayout() - hostingController.view.frame = view.bounds - } - - private func didEmit(rendering: T.Rendering) { - rootViewProvider.view = content(rendering) - } - - private func didEmit(output: T.Output) { - onOutput(output) - } - - func update(to workflow: T) { - workflowHost.update(workflow: workflow) - } - } - - #endif - - // Assigning `rootView` on a `UIHostingController` causes unwanted animated transitions. - // To avoid this, we never change the root view, but we pass down an `ObservableObject` - // so that we can still update the hierarchy as the workflow emits new renderings. - @available(iOS 13.0, macOS 10.15, *) - fileprivate final class RootViewProvider: ObservableObject { - @Published var view: T - - init(view: T) { - self.view = view - } - } - - @available(iOS 13.0, macOS 10.15, *) - fileprivate struct RootView: View { - @ObservedObject var provider: RootViewProvider - - var body: some View { - provider.view - } - } - -#endif diff --git a/swift/WorkflowTesting/Sources/RenderExpectations.swift b/swift/WorkflowTesting/Sources/RenderExpectations.swift deleted file mode 100644 index 54d98c2cf..000000000 --- a/swift/WorkflowTesting/Sources/RenderExpectations.swift +++ /dev/null @@ -1,137 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 Workflow - -/// A set of expectations for use with the `WorkflowRenderTester`. All of the expectations must be fulfilled -/// for a `render` test to pass. -public struct RenderExpectations { - var expectedState: ExpectedState? - var expectedOutput: ExpectedOutput? - var expectedWorkers: [ExpectedWorker] - var expectedWorkflows: [ExpectedWorkflow] - var expectedSideEffects: [AnyHashable: ExpectedSideEffect] - - public init( - expectedState: ExpectedState? = nil, - expectedOutput: ExpectedOutput? = nil, - expectedWorkers: [ExpectedWorker] = [], - expectedWorkflows: [ExpectedWorkflow] = [], - expectedSideEffects: [ExpectedSideEffect] = [] - ) { - self.expectedState = expectedState - self.expectedOutput = expectedOutput - self.expectedWorkers = expectedWorkers - self.expectedWorkflows = expectedWorkflows - self.expectedSideEffects = expectedSideEffects.reduce(into: [AnyHashable: ExpectedSideEffect]()) { res, expectedSideEffect in - res[expectedSideEffect.key] = expectedSideEffect - } - } -} - -public struct ExpectedOutput { - let output: WorkflowType.Output - let isEquivalent: (WorkflowType.Output, WorkflowType.Output) -> Bool - - public init(output: Output, isEquivalent: @escaping (Output, Output) -> Bool) where Output == WorkflowType.Output { - self.output = output - self.isEquivalent = isEquivalent - } - - public init(output: Output) where Output == WorkflowType.Output, Output: Equatable { - self.init(output: output, isEquivalent: { expected, actual in - expected == actual - }) - } -} - -public struct ExpectedState { - let state: WorkflowType.State - let isEquivalent: (WorkflowType.State, WorkflowType.State) -> Bool - - /// Create a new expected state from a state with an equivalence block. `isEquivalent` will be - /// called to validate that the expected state matches the actual state after a render pass. - public init(state: State, isEquivalent: @escaping (State, State) -> Bool) where State == WorkflowType.State { - self.state = state - self.isEquivalent = isEquivalent - } - - public init(state: State) where WorkflowType.State == State, State: Equatable { - self.init(state: state, isEquivalent: { expected, actual in - expected == actual - }) - } -} - -public struct ExpectedWorker { - let worker: Any - private let output: Any? - - /// Create a new expected worker with an optional output. If `output` is not nil, it will be emitted - /// when this worker is declared in the render pass. - public init(worker: WorkerType, output: WorkerType.Output? = nil) { - self.worker = worker - self.output = output - } - - func isEquivalent(to actual: WorkerType) -> Bool { - guard let expectedWorker = worker as? WorkerType else { - return false - } - - return expectedWorker.isEquivalent(to: actual) - } - - func outputAction(outputMap: (Output) -> ActionType) -> ActionType? where ActionType: WorkflowAction { - guard let output = output as? Output else { - return nil - } - - return outputMap(output) - } -} - -public struct ExpectedSideEffect { - let key: AnyHashable - let action: ((RenderContext) -> Void)? -} - -extension ExpectedSideEffect { - public init(key: AnyHashable) { - self.init(key: key) { _ in } - } - - public init(key: AnyHashable, action: ActionType) where ActionType.WorkflowType == WorkflowType { - self.init(key: key) { context in - let sink = context.makeSink(of: ActionType.self) - sink.send(action) - } - } -} - -public struct ExpectedWorkflow { - let workflowType: Any.Type - let key: String - let rendering: Any - let output: Any? - - public init(type: WorkflowType.Type, key: String = "", rendering: WorkflowType.Rendering, output: WorkflowType.Output? = nil) { - self.workflowType = type - self.key = key - self.rendering = rendering - self.output = output - } -} diff --git a/swift/WorkflowTesting/Sources/WorkflowActionTester.swift b/swift/WorkflowTesting/Sources/WorkflowActionTester.swift deleted file mode 100644 index 0b9dc5654..000000000 --- a/swift/WorkflowTesting/Sources/WorkflowActionTester.swift +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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. - */ - -// WorkflowTesting only available in Debug mode. -// -// `@testable import Workflow` will fail compilation in Release mode. -#if DEBUG - - @testable import Workflow - - extension WorkflowAction { - /// Returns a state tester containing `self`. - public static func tester(withState state: WorkflowType.State) -> WorkflowActionTester { - return WorkflowActionTester(state: state) - } - } - - /// Testing helper that chains event sending and state/output assertions - /// to make tests easier to write. - /// - /// ``` - /// reducer - /// .tester(withState: .firstState) - /// .assertState { state in - /// XCTAssertEqual(.firstState, state) - /// } - /// .send(event: .exampleEvent) { output in - /// XCTAssertEqual(.finished, output) - /// } - /// .assertState { state in - /// XCTAssertEqual(.differentState, state) - /// } - /// ``` - public struct WorkflowActionTester where Action: WorkflowAction, Action.WorkflowType == WorkflowType { - /// The current state - internal let state: WorkflowType.State - - /// Initializes a new state tester - fileprivate init(state: WorkflowType.State) { - self.state = state - } - - /// Sends an event to the reducer. - /// - /// - parameter event: The event to send. - /// - /// - parameter outputAssertions: An optional closure that runs validations on the output generated by the reducer. - /// - /// - returns: A new state tester containing the state after the update. - @discardableResult - public func send(action: Action, outputAssertions: (WorkflowType.Output?) -> Void = { _ in }) -> WorkflowActionTester { - var newState = state - let output = action.apply(toState: &newState) - - outputAssertions(output) - - return WorkflowActionTester(state: newState) - } - - /// Invokes the given closure (which is intended to contain test assertions) with the current state. - /// - /// - parameter assertions: A closure that accepts a single state value. - /// - /// - returns: A tester containing the current state. - @discardableResult - public func assertState(_ assertions: (WorkflowType.State) -> Void) -> WorkflowActionTester { - assertions(state) - return self - } - } - -#endif diff --git a/swift/WorkflowTesting/Sources/WorkflowRenderTester.swift b/swift/WorkflowTesting/Sources/WorkflowRenderTester.swift deleted file mode 100644 index 79f206758..000000000 --- a/swift/WorkflowTesting/Sources/WorkflowRenderTester.swift +++ /dev/null @@ -1,352 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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. - */ - -#if DEBUG - - // WorkflowTesting only available in Debug mode. -// - // `@testable import Workflow` will fail compilation in Release mode. - @testable import Workflow - - import ReactiveSwift - import class Workflow.Lifetime - import XCTest - - extension Workflow { - /// Returns a `RenderTester` with a specified initial state. - public func renderTester(initialState: Self.State) -> RenderTester { - return RenderTester(workflow: self, state: initialState) - } - - /// Returns a `RenderTester` with an initial state provided by `self.makeInitialState()` - public func renderTester() -> RenderTester { - return renderTester(initialState: makeInitialState()) - } - } - - /// Testing helper for validating the behavior of calls to `render`. - /// - /// Usage: Set up a set of `RenderExpectations` and then validate with a call to `render`. - /// Side-effects may be performed against the rendering to validate the behavior of actions. - /// - /// There is also a convenience `render` method where each expectation - /// is an individual parameter. - /// - /// Child workflows will always be rendered based upon their initial state. - /// - /// To directly test actions and their effects, use the `WorkflowActionTester`. - /// - /// ``` - /// workflow - /// .renderTester(initialState: TestWorkflow.State()) - /// .render( - /// with: RenderExpectations( - /// expectedState: ExpectedState(state: TestWorkflow.State()), - /// expectedOutput: ExpectedOutput(output: TestWorkflow.Output.finished), - /// expectedWorkers: [ - /// ExpectedWorker( - /// worker: TestWorker(), - /// output: TestWorker.Output.success), - /// ..., - /// ] - /// expectedWorkflows: [ - /// ExpectedWorkflow( - /// type: ChildWorkflow.self, - /// key: "key", - /// rendering: "rendering", - /// output: ChildWorkflow.Output.success), - /// ..., - /// ]), - /// assertions: { rendering in - /// XCTAssertEqual("expected text on rendering", rendering.text) - /// } - /// .render(...) // continue testing. The state will be updated based on actions or outputs. - /// ``` - /// - /// Using the convenience API - /// ``` - /// workflow - /// .renderTester(initialState: TestWorkflow.State()) - /// .render( - /// expectedState: ExpectedState(state: TestWorkflow.State()), - /// expectedOutput: ExpectedOutput(output: TestWorkflow.Output.finished), - /// expectedWorkers: [ - /// ExpectedWorker( - /// worker: TestWorker(), - /// output: TestWorker.Output.success), - /// ..., - /// ] - /// expectedWorkflows: [ - /// ExpectedWorkflow( - /// type: ChildWorkflow.self, - /// key: "key", - /// rendering: "rendering", - /// output: ChildWorkflow.Output.success) - /// ..., - /// ], - /// assertions: { rendering in - /// XCTAssertEqual("expected text on rendering", rendering.text) - /// } - /// .render(...) // continue testing. The state will be updated based on actions or outputs. - /// ``` - /// - /// Validating the rendering only from the initial state provided by the workflow: - /// ``` - /// workflow - /// .renderTester() - /// .render( - /// with: RenderExpectations(), - /// assertions: { rendering in - /// XCTAssertEqual("expected text on rendering", rendering.text) - /// } - /// ``` - /// - /// Validate the state was updated from a callback on the rendering: - /// ``` - /// workflow - /// .renderTester() - /// .render( - /// with: RenderExpectations( - /// expectedState: ExpectedState(state: TestWorkflow.State(text: "updated")), - /// assertions: { rendering in - /// XCTAssertEqual("expected text on rendering", rendering.text) - /// rendering.updateText("updated") - /// } - /// ``` - /// - /// Validate an output was received from the workflow. The `action()` on the rendering will cause an action that will return an output. - /// ``` - /// workflow - /// .renderTester() - /// .render( - /// with: RenderExpectations( - /// expectedState: ExpectedOutput(output: .success) - /// assertions: { rendering in - /// rendering.action() - /// } - /// ``` - /// - /// Validate a worker is running, and simulate the effect of its output: - /// ``` - /// workflow - /// .renderTester(initialState: TestWorkflow.State(loadingState: .loading)) - /// .render( - /// with: RenderExpectations( - /// expectedState: ExpectedState(state: TestWorkflow.State(loadingState: .idle)), - /// expectedWorkers: [ - /// ExpectedWorker( - /// worker: TestWorker(), - /// output: TestWorker.Output.success), - /// ..., - /// ]), - /// assertions: {} - /// ``` - /// - /// Validate a child workflow is run, and simulate the effect of its output: - /// ``` - /// workflow - /// .renderTester(initialState: TestWorkflow.State(loadingState: .loading)) - /// .render( - /// with: RenderExpectations( - /// expectedState: ExpectedState(state: TestWorkflow.State(loadingState: .idle)), - /// expectedWorkflows: [ - /// ExpectedWorkflow( - /// type: ChildWorkflow.self, - /// rendering: "rendering", - /// output: ChildWorkflow.Output.success - /// ]), - /// assertions: {} - /// ``` - public final class RenderTester { - private var workflow: WorkflowType - private var state: WorkflowType.State - - init(workflow: WorkflowType, state: WorkflowType.State) { - self.workflow = workflow - self.state = state - } - - /// Call `render` with a set of expectations. If the expectations have not been fulfilled, the test will fail. - @discardableResult - public func render(file: StaticString = #file, line: UInt = #line, with expectations: RenderExpectations, assertions: (WorkflowType.Rendering) -> Void) -> RenderTester { - let testContext = RenderTestContext( - state: state, - expectations: expectations, - file: file, - line: line - ) - - let context = RenderContext.make(implementation: testContext) - let rendering = workflow.render(state: testContext.state, context: context) - - assertions(rendering) - testContext.assertExpectations() - - state = testContext.state - - return self - } - - /// Convenience method for testing without creating an explicit RenderExpectation. - @discardableResult - public func render( - file: StaticString = #file, line: UInt = #line, - expectedState: ExpectedState? = nil, - expectedOutput: ExpectedOutput? = nil, - expectedWorkers: [ExpectedWorker] = [], - expectedWorkflows: [ExpectedWorkflow] = [], - expectedSideEffects: [ExpectedSideEffect] = [], - assertions: (WorkflowType.Rendering) -> Void - ) -> RenderTester { - let expectations = RenderExpectations( - expectedState: expectedState, - expectedOutput: expectedOutput, - expectedWorkers: expectedWorkers, - expectedWorkflows: expectedWorkflows, - expectedSideEffects: expectedSideEffects - ) - - return render(file: file, line: line, with: expectations, assertions: assertions) - } - - /// Assert the internal state. - @discardableResult - public func assert(state assertions: (WorkflowType.State) -> Void) -> RenderTester { - assertions(state) - return self - } - } - - fileprivate final class RenderTestContext: RenderContextType { - typealias WorkflowType = T - - private var (lifetime, token) = ReactiveSwift.Lifetime.make() - - var state: WorkflowType.State - var expectations: RenderExpectations - let file: StaticString - let line: UInt - - init(state: WorkflowType.State, expectations: RenderExpectations, file: StaticString, line: UInt) { - self.state = state - self.expectations = expectations - self.file = file - self.line = line - } - - func render(workflow: Child, key: String, outputMap: @escaping (Child.Output) -> Action) -> Child.Rendering where Child: Workflow, Action: WorkflowAction, RenderTestContext.WorkflowType == Action.WorkflowType { - guard let workflowIndex = expectations.expectedWorkflows.firstIndex(where: { expectedWorkflow -> Bool in - type(of: workflow) == expectedWorkflow.workflowType && key == expectedWorkflow.key - }) else { - XCTFail("Unexpected child workflow of type \(workflow.self)", file: file, line: line) - fatalError() - } - - let expectedWorkflow = expectations.expectedWorkflows.remove(at: workflowIndex) - if let childOutput = expectedWorkflow.output as? Child.Output { - apply(action: outputMap(childOutput)) - } - return expectedWorkflow.rendering as! Child.Rendering - } - - func makeSink(of actionType: Action.Type) -> Sink where Action: WorkflowAction, T == Action.WorkflowType { - let (signal, observer) = Signal, Never>.pipe() - let sink = Sink { action in - observer.send(value: AnyWorkflowAction(action)) - } - - signal - .take(during: lifetime) - .observeValues { [weak self] action in - self?.apply(action: action) - } - - return sink - } - - func awaitResult(for worker: W, outputMap: @escaping (W.Output) -> Action) where W: Worker, Action: WorkflowAction, RenderTestContext.WorkflowType == Action.WorkflowType { - guard let workerIndex = expectations.expectedWorkers.firstIndex(where: { (expectedWorker) -> Bool in - expectedWorker.isEquivalent(to: worker) - }) else { - XCTFail("Unexpected worker during render \(worker)", file: file, line: line) - return - } - - let expectedWorker = expectations.expectedWorkers.remove(at: workerIndex) - if let action = expectedWorker.outputAction(outputMap: outputMap) { - apply(action: action) - } - } - - func runSideEffect(key: AnyHashable, action: (Lifetime) -> Void) { - guard let sideEffect = expectations.expectedSideEffects.removeValue(forKey: key) else { - XCTFail("Unexpected side-effect during render \(key)", file: file, line: line) - return - } - - sideEffect.action?(RenderContext.make(implementation: self)) - } - - private func apply(action: Action) where Action: WorkflowAction, Action.WorkflowType == WorkflowType { - let output = action.apply(toState: &state) - switch (output, expectations.expectedOutput) { - case (.none, .none): - // No expected output, no output received. - break - - case (.some, .none): - XCTFail("Received an output, but expected no output.", file: file, line: line) - - case (.none, .some): - XCTFail("Expected an output, but received none.", file: file, line: line) - - case let (.some(output), .some(expectedOutput)): - XCTAssertTrue(expectedOutput.isEquivalent(output, expectedOutput.output), "expect output of \(expectedOutput.output) but received \(output)", file: file, line: line) - } - expectations.expectedOutput = nil - } - - /// Validate the expectations were fulfilled, or fail if not. - func assertExpectations() { - if let expectedState = expectations.expectedState { - XCTAssertTrue(expectedState.isEquivalent(expectedState.state, state), "State: \(state) was not equivalent to expected state: \(expectedState.state)", file: file, line: line) - } - - if let outputExpectation = expectations.expectedOutput { - XCTFail("Expected output of '\(outputExpectation.output)' but received none.", file: file, line: line) - } - - if !expectations.expectedWorkers.isEmpty { - for expectedWorker in expectations.expectedWorkers { - XCTFail("Expected worker \(expectedWorker.worker)", file: file, line: line) - } - } - - if !expectations.expectedWorkflows.isEmpty { - for expectedWorkflow in expectations.expectedWorkflows { - XCTFail("Expected child workflow of type: \(expectedWorkflow.workflowType) key: \(expectedWorkflow.key)", file: file, line: line) - } - } - - if !expectations.expectedSideEffects.isEmpty { - for expectedSideEffect in expectations.expectedSideEffects { - XCTFail("Expected side-effect with key: \(expectedSideEffect.key)", file: file, line: line) - } - } - } - } - -#endif diff --git a/swift/WorkflowTesting/Tests/WorkflowActionTesterTests.swift b/swift/WorkflowTesting/Tests/WorkflowActionTesterTests.swift deleted file mode 100644 index 050102bf9..000000000 --- a/swift/WorkflowTesting/Tests/WorkflowActionTesterTests.swift +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 ReactiveSwift -import Workflow -import XCTest -@testable import WorkflowTesting - -final class WorkflowActionTesterTests: XCTestCase { - func test_stateTransitions() { - TestAction - .tester(withState: false) - .assertState { XCTAssertFalse($0) } - .send(action: .toggleTapped) - .assertState { XCTAssertTrue($0) } - } - - func test_outputs() { - TestAction - .tester(withState: false) - .send(action: .exitTapped) { output in - XCTAssertEqual(output, .finished) - } - } - - func test_testerExtension() { - let state = true - let tester = TestAction - .tester(withState: true) - XCTAssertEqual(state, tester.state) - } -} - -private enum TestAction: WorkflowAction { - case toggleTapped - case exitTapped - - typealias WorkflowType = TestWorkflow - - func apply(toState state: inout Bool) -> TestWorkflow.Output? { - switch self { - case .toggleTapped: - state = !state - return nil - case .exitTapped: - return .finished - } - } -} - -private struct TestWorkflow: Workflow { - typealias State = Bool - - enum Output { - case finished - } - - func makeInitialState() -> Bool { - return true - } - - func render(state: Bool, context: RenderContext) { - return () - } -} diff --git a/swift/WorkflowTesting/Tests/WorkflowRenderTesterTests.swift b/swift/WorkflowTesting/Tests/WorkflowRenderTesterTests.swift deleted file mode 100644 index b0a9e0c74..000000000 --- a/swift/WorkflowTesting/Tests/WorkflowRenderTesterTests.swift +++ /dev/null @@ -1,459 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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 ReactiveSwift -import Workflow -import WorkflowTesting -import XCTest - -final class WorkflowRenderTesterTests: XCTestCase { - func test_assertState() { - let renderTester = TestWorkflow(initialText: "initial").renderTester() - var testedAssertion = false - - renderTester.assert { state in - XCTAssertEqual("initial", state.text) - XCTAssertEqual(.idle, state.substate) - testedAssertion = true - } - XCTAssertTrue(testedAssertion) - } - - func test_render() { - let renderTester = TestWorkflow(initialText: "initial").renderTester() - var testedAssertion = false - - renderTester.render( - with: RenderExpectations( - expectedState: ExpectedState( - state: TestWorkflow.State( - text: "initial", - substate: .idle - ) - ) - ), - assertions: { screen in - XCTAssertEqual("initial", screen.text) - testedAssertion = true - } - ) - XCTAssertTrue(testedAssertion) - } - - func test_simple_render() { - let renderTester = TestWorkflow(initialText: "initial").renderTester() - - renderTester.render { screen in - XCTAssertEqual("initial", screen.text) - } - } - - func test_action() { - let renderTester = TestWorkflow(initialText: "initial").renderTester() - - renderTester.render( - with: RenderExpectations( - expectedState: ExpectedState( - state: TestWorkflow.State( - text: "initial", - substate: .waiting - ) - ) - ), - assertions: { screen in - XCTAssertEqual("initial", screen.text) - screen.tapped() - } - ) - } - - func test_sideEffects() { - let renderTester = SideEffectWorkflow().renderTester() - - renderTester.render( - with: RenderExpectations( - expectedState: ExpectedState(state: .success), - expectedSideEffects: [ - ExpectedSideEffect(key: TestSideEffectKey(), action: SideEffectWorkflow.Action.testAction), - ] - ), - assertions: { _ in } - ) - } - - func test_output() { - OutputWorkflow() - .renderTester() - .render( - with: RenderExpectations( - expectedOutput: ExpectedOutput(output: .success) - ), - assertions: { rendering in - rendering.tapped() - } - ) - } - - func test_workers() { - let renderTester = TestWorkflow(initialText: "initial") - .renderTester( - initialState: TestWorkflow.State( - text: "otherText", - substate: .waiting - ) - ) - - let expectedWorker = ExpectedWorker(worker: TestWorker(text: "otherText")) - - renderTester.render( - with: RenderExpectations( - expectedState: nil, - expectedWorkers: [expectedWorker] - ), - assertions: { screen in - XCTAssertEqual("otherText", screen.text) - } - ) - } - - func test_workerOutput() { - let renderTester = TestWorkflow(initialText: "initial") - .renderTester(initialState: TestWorkflow.State( - text: "otherText", - substate: .waiting - )) - - let expectedWorker = ExpectedWorker(worker: TestWorker(text: "otherText"), output: .success) - let expectedState = ExpectedState(state: TestWorkflow.State(text: "otherText", substate: .idle)) - - renderTester.render( - with: RenderExpectations( - expectedState: expectedState, - expectedWorkers: [expectedWorker] - ), - assertions: { screen in - XCTAssertEqual("otherText", screen.text) - } - ) - } - - func test_childWorkflow() { - // Test the child independently from the parent. - ChildWorkflow(text: "hello") - .renderTester() - .render( - with: RenderExpectations( - expectedOutput: ExpectedOutput(output: .success), - expectedWorkers: [ - ExpectedWorker( - worker: TestWorker(text: "hello"), - output: .success - ), - ] - ), - assertions: { rendering in - XCTAssertEqual("olleh", rendering) - } - ) - - // Test the parent simulating the behavior of the child. The worker would run, but because the child is simulated, does not run. - ParentWorkflow(initialText: "hello") - .renderTester() - .render( - with: RenderExpectations(expectedWorkflows: [ - ExpectedWorkflow( - type: ChildWorkflow.self, - rendering: "olleh", - output: nil - ), - ]), - assertions: { rendering in - XCTAssertEqual("olleh", rendering) - } - ) - } - - func test_childWorkflowOutput() { - // Test that a child emitting an output is handled as an action by the parent - ParentWorkflow(initialText: "hello") - .renderTester() - .render( - expectedState: ExpectedState(state: ParentWorkflow.State(text: "Failed")), - expectedWorkflows: [ - ExpectedWorkflow( - type: ChildWorkflow.self, - rendering: "olleh", - output: .failure - ), - ], - assertions: { rendering in - XCTAssertEqual("olleh", rendering) - } - ) - .assert { state in - XCTAssertEqual("Failed", state.text) - } - } - - func test_implict_expectations() { - TestWorkflow(initialText: "hello") - .renderTester() - .render( - expectedState: ExpectedState( - state: TestWorkflow.State( - text: "hello", - substate: .idle - ) - ), - expectedOutput: nil, - expectedWorkers: [], - expectedWorkflows: [], - assertions: { rendering in - XCTAssertEqual("hello", rendering.text) - } - ) - } -} - -private struct TestWorkflow: Workflow { - /// Input - var initialText: String - - /// Output - enum Output: Equatable { - case first - } - - struct State: Equatable { - var text: String - var substate: Substate - enum Substate: Equatable { - case idle - case waiting - } - } - - func makeInitialState() -> State { - return State(text: initialText, substate: .idle) - } - - func render(state: State, context: RenderContext) -> TestScreen { - let sink = context.makeSink(of: Action.self) - - switch state.substate { - case .idle: - break - case .waiting: - context.awaitResult(for: TestWorker(text: state.text)) { output -> Action in - .asyncSuccess - } - } - - return TestScreen( - text: state.text, - tapped: { - sink.send(.tapped) - } - ) - } -} - -extension TestWorkflow { - enum Action: WorkflowAction, Equatable { - typealias WorkflowType = TestWorkflow - - case tapped - case asyncSuccess - - func apply(toState state: inout TestWorkflow.State) -> TestWorkflow.Output? { - switch self { - case .tapped: - state.substate = .waiting - - case .asyncSuccess: - state.substate = .idle - } - return nil - } - } -} - -private struct OutputWorkflow: Workflow { - enum Output { - case success - case failure - } - - struct State {} - - func makeInitialState() -> OutputWorkflow.State { - return State() - } - - enum Action: WorkflowAction { - typealias WorkflowType = OutputWorkflow - - case emit - - func apply(toState state: inout OutputWorkflow.State) -> OutputWorkflow.Output? { - switch self { - case .emit: - return .success - } - } - } - - typealias Rendering = TestScreen - - func render(state: State, context: RenderContext) -> TestScreen { - let sink = context.makeSink(of: Action.self) - - return TestScreen(text: "value", tapped: { - sink.send(.emit) - }) - } -} - -private struct TestSideEffectKey: Hashable { - let key: String = "Test Side Effect" -} - -private struct SideEffectWorkflow: Workflow { - enum State: Equatable { - case idle - case success - } - - enum Action: WorkflowAction { - case testAction - - typealias WorkflowType = SideEffectWorkflow - - func apply(toState state: inout SideEffectWorkflow.State) -> SideEffectWorkflow.Output? { - switch self { - case .testAction: - state = .success - } - return nil - } - } - - typealias Rendering = TestScreen - - func render(state: State, context: RenderContext) -> TestScreen { - context.runSideEffect(key: TestSideEffectKey()) { _ in } - - return TestScreen(text: "value", tapped: {}) - } - - func makeInitialState() -> State { - .idle - } -} - -private struct TestWorker: Worker { - var text: String - - enum Output { - case success - case failure - } - - func run() -> SignalProducer { - return SignalProducer(value: .success) - } - - func isEquivalent(to otherWorker: TestWorker) -> Bool { - return text == otherWorker.text - } -} - -private struct TestScreen { - var text: String - var tapped: () -> Void -} - -private struct ParentWorkflow: Workflow { - typealias Output = Never - - var initialText: String - - struct State: Equatable { - var text: String - } - - func makeInitialState() -> ParentWorkflow.State { - return State(text: initialText) - } - - enum Action: WorkflowAction { - typealias WorkflowType = ParentWorkflow - - case childSuccess - case childFailure - - func apply(toState state: inout ParentWorkflow.State) -> Never? { - switch self { - case .childSuccess: - state.text = String(state.text.reversed()) - - case .childFailure: - state.text = "Failed" - } - - return nil - } - } - - func render(state: ParentWorkflow.State, context: RenderContext) -> String { - return ChildWorkflow(text: state.text) - .mapOutput { output -> Action in - switch output { - case .success: - return .childSuccess - case .failure: - return .childFailure - } - } - .rendered(with: context) - } -} - -private struct ChildWorkflow: Workflow { - enum Output: Equatable { - case success - case failure - } - - var text: String - - struct State {} - - func makeInitialState() -> ChildWorkflow.State { - return State() - } - - func render(state: ChildWorkflow.State, context: RenderContext) -> String { - context.awaitResult( - for: TestWorker(text: text), - onOutput: { (output, state) -> Output in - .success - } - ) - - return String(text.reversed()) - } -} diff --git a/swift/WorkflowUI/Sources/Container/ContainerViewController+AnyWorkflow.swift b/swift/WorkflowUI/Sources/Container/ContainerViewController+AnyWorkflow.swift deleted file mode 100644 index de1602918..000000000 --- a/swift/WorkflowUI/Sources/Container/ContainerViewController+AnyWorkflow.swift +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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. - */ - -#if canImport(UIKit) - - import Foundation - import Workflow - - extension ContainerViewController { - public convenience init( - workflow: W, - rootViewEnvironment: ViewEnvironment = .empty - ) where W.Rendering == ScreenType, W.Output == Output { - self.init(workflow: WrapperWorkflow(workflow), rootViewEnvironment: rootViewEnvironment) - } - } - - fileprivate struct WrapperWorkflow: Workflow { - typealias State = Void - typealias Output = Output - typealias Rendering = Rendering - - var wrapped: AnyWorkflow - - init(_ wrapped: W) where W.Output == Output, W.Rendering == Rendering { - self.wrapped = wrapped.asAnyWorkflow() - } - - func render(state: State, context: RenderContext) -> Rendering { - return wrapped - .mapOutput { AnyWorkflowAction(sendingOutput: $0) } - .rendered(with: context) - } - } - -#endif diff --git a/swift/WorkflowUI/Sources/Container/ContainerViewController.swift b/swift/WorkflowUI/Sources/Container/ContainerViewController.swift deleted file mode 100644 index e1d24c0cb..000000000 --- a/swift/WorkflowUI/Sources/Container/ContainerViewController.swift +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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. - */ - -#if canImport(UIKit) - - import ReactiveSwift - import UIKit - import Workflow - - /// Drives view controllers from a root Workflow. - public final class ContainerViewController: UIViewController where ScreenType: Screen { - /// Emits output events from the bound workflow. - public let output: Signal - - internal let rootViewController: DescribedViewController - - private let workflowHost: Any - - private let rendering: Property - - private let (lifetime, token) = Lifetime.make() - - public var rootViewEnvironment: ViewEnvironment { - didSet { - // Re-render the current rendering with the new environment - render(screen: rendering.value, environment: rootViewEnvironment) - } - } - - private init(workflowHost: Any, rendering: Property, output: Signal, rootViewEnvironment: ViewEnvironment) { - self.workflowHost = workflowHost - self.rootViewController = DescribedViewController(screen: rendering.value, environment: rootViewEnvironment) - self.rendering = rendering - self.output = output - self.rootViewEnvironment = rootViewEnvironment - - super.init(nibName: nil, bundle: nil) - - rendering - .signal - .take(during: lifetime) - .observeValues { [weak self] screen in - guard let self = self else { return } - self.render(screen: screen, environment: self.rootViewEnvironment) - } - } - - public convenience init(workflow: W, rootViewEnvironment: ViewEnvironment = .empty) where W.Rendering == ScreenType, W.Output == Output { - let host = WorkflowHost(workflow: workflow) - self.init( - workflowHost: host, - rendering: host.rendering, - output: host.output, - rootViewEnvironment: rootViewEnvironment - ) - } - - public required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func render(screen: ScreenType, environment: ViewEnvironment) { - rootViewController.update(screen: screen, environment: environment) - } - - override public func viewDidLoad() { - super.viewDidLoad() - - view.backgroundColor = .white - - addChild(rootViewController) - view.addSubview(rootViewController.view) - rootViewController.didMove(toParent: self) - } - - override public func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - rootViewController.view.frame = view.bounds - } - - override public var childForStatusBarStyle: UIViewController? { - return rootViewController - } - - override public var childForStatusBarHidden: UIViewController? { - return rootViewController - } - - override public var childForHomeIndicatorAutoHidden: UIViewController? { - return rootViewController - } - - override public var childForScreenEdgesDeferringSystemGestures: UIViewController? { - return rootViewController - } - - override public var supportedInterfaceOrientations: UIInterfaceOrientationMask { - return rootViewController.supportedInterfaceOrientations - } - } - -#endif diff --git a/swift/WorkflowUI/Sources/Screen/AnyScreen/AnyScreen.swift b/swift/WorkflowUI/Sources/Screen/AnyScreen/AnyScreen.swift deleted file mode 100644 index d4c85cfd7..000000000 --- a/swift/WorkflowUI/Sources/Screen/AnyScreen/AnyScreen.swift +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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. - */ - -#if canImport(UIKit) - - import UIKit - - public struct AnyScreen: Screen { - /// The original screen, retained for debugging - internal let wrappedScreen: Screen - - /// Stored getter for the wrapped screen’s view controller description - private let _viewControllerDescription: (ViewEnvironment) -> ViewControllerDescription - - public init(_ screen: T) { - if let anyScreen = screen as? AnyScreen { - self = anyScreen - return - } - self.wrappedScreen = screen - self._viewControllerDescription = screen.viewControllerDescription(environment:) - } - - public func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { - // Passed straight through - return _viewControllerDescription(environment) - } - } - - extension Screen { - /// Wraps the screen in an AnyScreen - public func asAnyScreen() -> AnyScreen { - AnyScreen(self) - } - } - -#endif diff --git a/swift/WorkflowUI/Sources/Screen/Screen.swift b/swift/WorkflowUI/Sources/Screen/Screen.swift deleted file mode 100644 index a57f61774..000000000 --- a/swift/WorkflowUI/Sources/Screen/Screen.swift +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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. - */ - -#if canImport(UIKit) - - /// Screens are the building blocks of an interactive application. - /// - /// Conforming types contain any information needed to populate a screen: data, - /// styling, event handlers, etc. - public protocol Screen { - /// A view controller description that acts as a recipe to either build - /// or update a previously-built view controller to match this screen. - func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription - } - -#endif diff --git a/swift/WorkflowUI/Sources/Screen/ScreenViewController.swift b/swift/WorkflowUI/Sources/Screen/ScreenViewController.swift deleted file mode 100644 index e88f4019a..000000000 --- a/swift/WorkflowUI/Sources/Screen/ScreenViewController.swift +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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. - */ - -#if canImport(UIKit) - - import UIKit - - /// Generic base class that can be subclassed in order to to define a UI implementation that is powered by the - /// given screen type. - /// - /// Using this base class, a screen can be implemented as: - /// ``` - /// struct MyScreen: Screen { - /// func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { - /// return MyScreenViewController.description(for: self) - /// } - /// } - /// - /// private class MyScreenViewController: ScreenViewController { - /// override func screenDidChange(from previousScreen: MyScreen, previousEnvironment: ViewEnvironment) { - /// // … update views as necessary - /// } - /// } - /// ``` - open class ScreenViewController: UIViewController { - public private(set) final var screen: ScreenType - - public final var screenType: Screen.Type { - return ScreenType.self - } - - public private(set) final var environment: ViewEnvironment - - public required init(screen: ScreenType, environment: ViewEnvironment) { - self.screen = screen - self.environment = environment - super.init(nibName: nil, bundle: nil) - } - - @available(*, unavailable) - public required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - public final func update(screen: ScreenType, environment: ViewEnvironment) { - let previousScreen = self.screen - self.screen = screen - let previousEnvironment = self.environment - self.environment = environment - screenDidChange(from: previousScreen, previousEnvironment: previousEnvironment) - } - - /// Subclasses should override this method in order to update any relevant UI bits when the screen model changes. - open func screenDidChange(from previousScreen: ScreenType, previousEnvironment: ViewEnvironment) {} - } - - extension ScreenViewController { - /// Convenience to create a view controller description for the given screen - /// value. See the example on the comment for ScreenViewController for - /// usage. - public final class func description(for screen: ScreenType, environment: ViewEnvironment) -> ViewControllerDescription { - return ViewControllerDescription( - type: self, - build: { self.init(screen: screen, environment: environment) }, - update: { $0.update(screen: screen, environment: environment) } - ) - } - } - -#endif diff --git a/swift/WorkflowUI/Sources/Screen/ViewEnvironment/ViewEnvironment.swift b/swift/WorkflowUI/Sources/Screen/ViewEnvironment/ViewEnvironment.swift deleted file mode 100644 index 1349fb456..000000000 --- a/swift/WorkflowUI/Sources/Screen/ViewEnvironment/ViewEnvironment.swift +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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. - */ - -/// ViewEnvironment acts as a container for values to flow down the view-side -/// of a rendering tree (as opposed to being passed down through Workflows). -/// -/// This will often be used by containers to let their children know in what -/// context they’re appearing (for example, a split screen container may set -/// the environment of its two children according to which position they’re -/// appearing in). -public struct ViewEnvironment { - /// An empty view environment. This should only be used when setting up a - /// root workflow into a root ContainerViewController or when writing tests. - /// In other scenarios, containers should pass down the ViewEnvironment - /// value they get from above. - public static let empty: ViewEnvironment = ViewEnvironment() - - /// Storage of [K.Type: K.Value] where K: ViewEnvironmentKey - private var storage: [ObjectIdentifier: Any] - - /// Private empty initializer to make the `empty` environment explicit. - private init() { - self.storage = [:] - } - - /// Get or set for the given ViewEnvironmentKey. - /// - /// This will typically only be used by the module that provides the - /// environment value. See documentation for ViewEnvironmentKey for a - /// usage example. - public subscript(key: Key.Type) -> Key.Value where Key: ViewEnvironmentKey { - get { - if let value = storage[ObjectIdentifier(key)] as? Key.Value { - return value - } else { - return Key.defaultValue - } - } - - set { - storage[ObjectIdentifier(key)] = newValue - } - } - - /// Returns a new ViewEnvironment with the given value set for the given - /// environment key. - /// - /// This is provided as a convenience for modifying the environment while - /// passing it down to children screens without the need for an intermediate - /// mutable value. It is functionally equivalent to the subscript setter. - public func setting(key: Key.Type, to value: Key.Value) -> ViewEnvironment where Key: ViewEnvironmentKey { - var newEnvironment = self - newEnvironment[key] = value - return newEnvironment - } - - /// Returns a new ViewEnvironment with the given value set for the given - /// key path. - /// - /// This is provided as a convenience for modifying the environment while - /// passing it down to children screens. - /// - /// The following are functionally equivalent: - /// ``` - /// var newEnvironment = environment - /// newEnvironment.someProperty = 42 - /// ``` - /// and - /// ``` - /// let newEnvironment = environment.setting(\.someProperty, to: 42) - /// ``` - /// - /// - public func setting(keyPath: WritableKeyPath, to value: Value) -> ViewEnvironment { - var newEnvironment = self - newEnvironment[keyPath: keyPath] = value - return newEnvironment - } -} diff --git a/swift/WorkflowUI/Sources/Screen/ViewEnvironment/ViewEnvironmentKey.swift b/swift/WorkflowUI/Sources/Screen/ViewEnvironment/ViewEnvironmentKey.swift deleted file mode 100644 index 02935fe59..000000000 --- a/swift/WorkflowUI/Sources/Screen/ViewEnvironment/ViewEnvironmentKey.swift +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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. - */ - -/// A key into the ViewEnvironment. -/// -/// Environment keys are associated with a specific type of value (`Value`) and -/// must declare a default value. -/// -/// Typically the key conforming to `ViewEnvironmentKey` will be private, and -/// you are encouraged to provide a convenience accessor on `ViewEnvironment` -/// as in the following example: -/// -/// ``` -/// private enum ThemeKey: ViewEnvironmentKey { -/// typealias Value = Theme -/// var defaultValue: Theme -/// } -/// -/// extension ViewEnvironment { -/// public var theme: Theme { -/// get { self[ThemeKey.self] } -/// set { self[ThemeKey.self] = newValue } -/// } -/// } -/// ``` -public protocol ViewEnvironmentKey { - associatedtype Value - - static var defaultValue: Value { get } -} diff --git a/swift/WorkflowUI/Sources/ViewControllerDescription/DescribedViewController.swift b/swift/WorkflowUI/Sources/ViewControllerDescription/DescribedViewController.swift deleted file mode 100644 index 7045944fe..000000000 --- a/swift/WorkflowUI/Sources/ViewControllerDescription/DescribedViewController.swift +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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. - */ - -#if canImport(UIKit) - - import UIKit - - public final class DescribedViewController: UIViewController { - var currentViewController: UIViewController - - public init(description: ViewControllerDescription) { - self.currentViewController = description.buildViewController() - super.init(nibName: nil, bundle: nil) - } - - public convenience init(screen: S, environment: ViewEnvironment) { - self.init(description: screen.viewControllerDescription(environment: environment)) - } - - @available(*, unavailable) - required init?(coder: NSCoder) { - fatalError("init(coder:) is unavailable") - } - - public func update(description: ViewControllerDescription) { - if description.canUpdate(viewController: currentViewController) { - description.update(viewController: currentViewController) - } else { - if isViewLoaded { - currentViewController.willMove(toParent: nil) - currentViewController.view.removeFromSuperview() - currentViewController.removeFromParent() - } - currentViewController = description.buildViewController() - if isViewLoaded { - addChild(currentViewController) - view.addSubview(currentViewController.view) - currentViewController.view.frame = view.bounds - currentViewController.didMove(toParent: self) - preferredContentSize = currentViewController.preferredContentSize - } - } - } - - public func update(screen: S, environment: ViewEnvironment) { - update(description: screen.viewControllerDescription(environment: environment)) - } - - override public func viewDidLoad() { - super.viewDidLoad() - - addChild(currentViewController) - view.addSubview(currentViewController.view) - currentViewController.didMove(toParent: self) - preferredContentSize = currentViewController.preferredContentSize - } - - override public func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - currentViewController.view.frame = view.bounds - } - - override public var childForStatusBarStyle: UIViewController? { - return currentViewController - } - - override public var childForStatusBarHidden: UIViewController? { - return currentViewController - } - - override public var childForHomeIndicatorAutoHidden: UIViewController? { - return currentViewController - } - - override public var childForScreenEdgesDeferringSystemGestures: UIViewController? { - return currentViewController - } - - override public var supportedInterfaceOrientations: UIInterfaceOrientationMask { - return currentViewController.supportedInterfaceOrientations - } - - override public func preferredContentSizeDidChange(forChildContentContainer container: UIContentContainer) { - super.preferredContentSizeDidChange(forChildContentContainer: container) - - guard - (container as? UIViewController) == currentViewController, - container.preferredContentSize != preferredContentSize - else { return } - - preferredContentSize = container.preferredContentSize - } - } - -#endif diff --git a/swift/WorkflowUI/Sources/ViewControllerDescription/ViewControllerDescription.swift b/swift/WorkflowUI/Sources/ViewControllerDescription/ViewControllerDescription.swift deleted file mode 100644 index 694d1f0d9..000000000 --- a/swift/WorkflowUI/Sources/ViewControllerDescription/ViewControllerDescription.swift +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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. - */ - -#if canImport(UIKit) - - import UIKit - - /// A ViewControllerDescription acts as a recipe for building and updating a - /// specific UIViewController. - public struct ViewControllerDescription { - private let viewControllerType: UIViewController.Type - private let build: () -> UIViewController - private let update: (UIViewController) -> Void - - /// Constructs a view controller description by providing a closure used to - /// build and update a specific view controller type. - /// - /// - Parameters: - /// - type: The type of view controller produced by this description. - /// Typically, should should be able to omit this parameter, but - /// in cases where type inference has trouble, it’s offered as - /// an escape hatch. - /// - build: Closure that produces a new instance of the view controller - /// - update: Closure that updates the given view controller - public init(type: VC.Type = VC.self, build: @escaping () -> VC, update: @escaping (VC) -> Void) { - self.viewControllerType = type - self.build = build - self.update = { untypedViewController in - guard let viewController = untypedViewController as? VC else { - fatalError("Unable to update \(untypedViewController), expecting a \(VC.self)") - } - update(viewController) - } - } - - /// Construct and update a new view controller as described by this view - /// controller description. - internal func buildViewController() -> UIViewController { - let viewController = build() - assert(canUpdate(viewController: viewController), "View controller description built a view controller it cannot update (\(viewController) is not exactly type \(viewControllerType))") - - // Perform an initial update of the built view controller - update(viewController: viewController) - - return viewController - } - - /// If the given view controller is of the correct type to be updated by - /// this view controller description. - internal func canUpdate(viewController: UIViewController) -> Bool { - return type(of: viewController) == viewControllerType - } - - /// Update the given view controller. - /// - /// - Note: Passing a view controller that does not return `true` from - /// `canUpdate(viewController:)` will result in an exception. - /// - /// - Parameter viewController: The view controller instance to update - internal func update(viewController: UIViewController) { - update(viewController) - } - } - -#endif diff --git a/swift/WorkflowUI/Tests/ContainerViewControllerTests.swift b/swift/WorkflowUI/Tests/ContainerViewControllerTests.swift deleted file mode 100644 index 230f1e60f..000000000 --- a/swift/WorkflowUI/Tests/ContainerViewControllerTests.swift +++ /dev/null @@ -1,137 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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. - */ - -#if canImport(UIKit) - - import XCTest - - import ReactiveSwift - import Workflow - @testable import WorkflowUI - - fileprivate struct TestScreen: Screen { - var string: String - - func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { - return TestScreenViewController.description(for: self, environment: environment) - } - } - - fileprivate final class TestScreenViewController: ScreenViewController { - var onScreenChange: (() -> Void)? - - override func screenDidChange(from previousScreen: TestScreen, previousEnvironment: ViewEnvironment) { - super.screenDidChange(from: previousScreen, previousEnvironment: previousEnvironment) - onScreenChange?() - } - } - - class ContainerViewControllerTests: XCTestCase { - func test_initialization_renders_workflow() { - let (signal, _) = Signal.pipe() - let workflow = MockWorkflow(subscription: signal) - let container = ContainerViewController(workflow: workflow) - - withExtendedLifetime(container) { - let vc = container.rootViewController.currentViewController as! TestScreenViewController - XCTAssertEqual("0", vc.screen.string) - } - } - - func test_workflow_update_causes_rerender() { - let (signal, observer) = Signal.pipe() - let workflow = MockWorkflow(subscription: signal) - let container = ContainerViewController(workflow: workflow) - - withExtendedLifetime(container) { - let expectation = XCTestExpectation(description: "View Controller updated") - - let vc = container.rootViewController.currentViewController as! TestScreenViewController - vc.onScreenChange = { - expectation.fulfill() - } - - observer.send(value: 2) - - wait(for: [expectation], timeout: 1.0) - - XCTAssertEqual("2", vc.screen.string) - } - } - - func test_workflow_output_causes_container_output() { - let (signal, observer) = Signal.pipe() - let workflow = MockWorkflow(subscription: signal) - let container = ContainerViewController(workflow: workflow) - - let expectation = XCTestExpectation(description: "Output") - - let disposable = container.output.observeValues { value in - XCTAssertEqual(3, value) - expectation.fulfill() - } - - observer.send(value: 3) - - wait(for: [expectation], timeout: 1.0) - - disposable?.dispose() - } - - func test_container_with_anyworkflow() { - let (signal, observer) = Signal.pipe() - let workflow = MockWorkflow(subscription: signal) - let container = ContainerViewController(workflow: workflow.asAnyWorkflow()) - - let expectation = XCTestExpectation(description: "Output") - - let disposable = container.output.observeValues { value in - XCTAssertEqual(3, value) - expectation.fulfill() - } - - observer.send(value: 3) - - wait(for: [expectation], timeout: 1.0) - - disposable?.dispose() - } - } - - fileprivate struct MockWorkflow: Workflow { - var subscription: Signal - - typealias State = Int - - typealias Output = Int - - func makeInitialState() -> State { - return 0 - } - - func render(state: State, context: RenderContext) -> TestScreen { - context.awaitResult(for: subscription.asWorker(key: "signal")) { output in - AnyWorkflowAction { state in - state = output - return output - } - } - - return TestScreen(string: "\(state)") - } - } - -#endif diff --git a/swift/WorkflowUI/Tests/DescribedViewControllerTests.swift b/swift/WorkflowUI/Tests/DescribedViewControllerTests.swift deleted file mode 100644 index 6134b0221..000000000 --- a/swift/WorkflowUI/Tests/DescribedViewControllerTests.swift +++ /dev/null @@ -1,333 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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. - */ - -#if canImport(UIKit) - - import XCTest - - import ReactiveSwift - import Workflow - @testable import WorkflowUI - - class DescribedViewControllerTests: XCTestCase { - // MARK: - Tests - - func test_init() { - // Given - let screen = TestScreen.counter(0) - - // When - let describedViewController = DescribedViewController(screen: screen, environment: .empty) - - // Then - guard - let currentViewController = describedViewController.currentViewController as? CounterViewController - else { - XCTFail("Expected a \(String(reflecting: CounterViewController.self)), but got: \(describedViewController.currentViewController)") - return - } - - XCTAssertEqual(currentViewController.count, 0) - XCTAssertFalse(describedViewController.isViewLoaded) - XCTAssertFalse(currentViewController.isViewLoaded) - XCTAssertNil(currentViewController.parent) - } - - func test_viewDidLoad() { - // Given - let screen = TestScreen.counter(0) - let describedViewController = DescribedViewController(screen: screen, environment: .empty) - - // When - _ = describedViewController.view - - // Then - XCTAssertEqual(describedViewController.currentViewController.parent, describedViewController) - XCTAssertNotNil(describedViewController.currentViewController.viewIfLoaded?.superview) - } - - func test_update_toCompatibleDescription_beforeViewLoads() { - // Given - let screenA = TestScreen.counter(0) - let screenB = TestScreen.counter(1) - - let describedViewController = DescribedViewController(screen: screenA, environment: .empty) - let initialChildViewController = describedViewController.currentViewController - - // When - describedViewController.update(screen: screenB, environment: .empty) - - // Then - XCTAssertEqual(initialChildViewController, describedViewController.currentViewController) - XCTAssertEqual((describedViewController.currentViewController as? CounterViewController)?.count, 1) - XCTAssertFalse(describedViewController.isViewLoaded) - XCTAssertFalse(describedViewController.currentViewController.isViewLoaded) - XCTAssertNil(describedViewController.currentViewController.parent) - } - - func test_update_toCompatibleDescription_afterViewLoads() { - // Given - let screenA = TestScreen.counter(0) - let screenB = TestScreen.counter(1) - - let describedViewController = DescribedViewController(screen: screenA, environment: .empty) - let initialChildViewController = describedViewController.currentViewController - - // When - _ = describedViewController.view - describedViewController.update(screen: screenB, environment: .empty) - - // Then - XCTAssertEqual(initialChildViewController, describedViewController.currentViewController) - XCTAssertEqual((describedViewController.currentViewController as? CounterViewController)?.count, 1) - } - - func test_update_toIncompatibleDescription_beforeViewLoads() { - // Given - let screenA = TestScreen.counter(0) - let screenB = TestScreen.message("Test") - - let describedViewController = DescribedViewController(screen: screenA, environment: .empty) - let initialChildViewController = describedViewController.currentViewController - - // When - describedViewController.update(screen: screenB, environment: .empty) - - // Then - XCTAssertNotEqual(initialChildViewController, describedViewController.currentViewController) - XCTAssertEqual((describedViewController.currentViewController as? MessageViewController)?.message, "Test") - XCTAssertFalse(describedViewController.isViewLoaded) - XCTAssertFalse(describedViewController.currentViewController.isViewLoaded) - XCTAssertNil(describedViewController.currentViewController.parent) - } - - func test_update_toIncompatibleDescription_afterViewLoads() { - // Given - let screenA = TestScreen.counter(0) - let screenB = TestScreen.message("Test") - - let describedViewController = DescribedViewController(screen: screenA, environment: .empty) - let initialChildViewController = describedViewController.currentViewController - - // When - _ = describedViewController.view - describedViewController.update(screen: screenB, environment: .empty) - - // Then - XCTAssertNotEqual(initialChildViewController, describedViewController.currentViewController) - XCTAssertEqual((describedViewController.currentViewController as? MessageViewController)?.message, "Test") - XCTAssertNil(initialChildViewController.parent) - XCTAssertEqual(describedViewController.currentViewController.parent, describedViewController) - XCTAssertNil(initialChildViewController.viewIfLoaded?.superview) - XCTAssertNotNil(describedViewController.currentViewController.viewIfLoaded?.superview) - } - - func test_childViewControllerFor() { - // Given - let screen = TestScreen.counter(0) - - let describedViewController = DescribedViewController(screen: screen, environment: .empty) - let currentViewController = describedViewController.currentViewController - - // When, Then - XCTAssertEqual(describedViewController.childForStatusBarStyle, currentViewController) - XCTAssertEqual(describedViewController.childForStatusBarHidden, currentViewController) - XCTAssertEqual(describedViewController.childForHomeIndicatorAutoHidden, currentViewController) - XCTAssertEqual(describedViewController.childForScreenEdgesDeferringSystemGestures, currentViewController) - XCTAssertEqual(describedViewController.supportedInterfaceOrientations, currentViewController.supportedInterfaceOrientations) - } - - func test_childViewControllerFor_afterIncompatibleUpdate() { - // Given - let screenA = TestScreen.counter(0) - let screenB = TestScreen.message("Test") - - let describedViewController = DescribedViewController(screen: screenA, environment: .empty) - let initialChildViewController = describedViewController.currentViewController - - describedViewController.update(screen: screenB, environment: .empty) - let currentViewController = describedViewController.currentViewController - - // When, Then - XCTAssertNotEqual(initialChildViewController, currentViewController) - XCTAssertEqual(describedViewController.childForStatusBarStyle, currentViewController) - XCTAssertEqual(describedViewController.childForStatusBarHidden, currentViewController) - XCTAssertEqual(describedViewController.childForHomeIndicatorAutoHidden, currentViewController) - XCTAssertEqual(describedViewController.childForScreenEdgesDeferringSystemGestures, currentViewController) - XCTAssertEqual(describedViewController.supportedInterfaceOrientations, currentViewController.supportedInterfaceOrientations) - } - - func test_preferredContentSizeDidChange() { - // Given - let screenA = TestScreen.counter(1) - let screenB = TestScreen.counter(2) - - let describedViewController = DescribedViewController(screen: screenA, environment: .empty) - let containerViewController = ContainerViewController(describedViewController: describedViewController) - - // When - let expectation = self.expectation(description: "did observe size changes") - expectation.expectedFulfillmentCount = 2 - - var observedSizes: [CGSize] = [] - let disposable = containerViewController.preferredContentSizeSignal.observeValues { - observedSizes.append($0) - expectation.fulfill() - } - - defer { disposable?.dispose() } - - _ = containerViewController.view - describedViewController.update(screen: screenB, environment: .empty) - - // Then - let expectedSizes = [CGSize(width: 10, height: 0), CGSize(width: 20, height: 0)] - waitForExpectations(timeout: 1, handler: nil) - XCTAssertEqual(observedSizes, expectedSizes) - } - - func test_preferredContentSizeDidChange_afterIncompatibleUpdate() { - // Given - let screenA = TestScreen.counter(1) - let screenB = TestScreen.message("Test") - let screenC = TestScreen.message("Testing") - - let describedViewController = DescribedViewController(screen: screenA, environment: .empty) - let containerViewController = ContainerViewController(describedViewController: describedViewController) - - // When - let expectation = self.expectation(description: "did observe size changes") - expectation.expectedFulfillmentCount = 3 - - var observedSizes: [CGSize] = [] - let disposable = containerViewController.preferredContentSizeSignal.observeValues { - observedSizes.append($0) - expectation.fulfill() - } - - defer { disposable?.dispose() } - - _ = containerViewController.view - describedViewController.update(screen: screenB, environment: .empty) - describedViewController.update(screen: screenC, environment: .empty) - - // Then - let expectedSizes = [ - CGSize(width: 10, height: 0), - CGSize(width: 40, height: 0), - CGSize(width: 70, height: 0), - ] - - waitForExpectations(timeout: 1, handler: nil) - XCTAssertEqual(observedSizes, expectedSizes) - } - } - - // MARK: - Helper Types - - fileprivate enum TestScreen: Screen, Equatable { - case counter(Int) - case message(String) - - func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { - switch self { - case let .counter(count): - return ViewControllerDescription( - build: { CounterViewController(count: count) }, - update: { $0.count = count } - ) - - case let .message(message): - return ViewControllerDescription( - build: { MessageViewController(message: message) }, - update: { $0.message = message } - ) - } - } - } - - fileprivate class ContainerViewController: UIViewController { - let describedViewController: DescribedViewController - - var preferredContentSizeSignal: Signal { return signal.skipRepeats() } - - private let (signal, sink) = Signal.pipe() - - init(describedViewController: DescribedViewController) { - self.describedViewController = describedViewController - super.init(nibName: nil, bundle: nil) - } - - @available(*, unavailable) required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - - addChild(describedViewController) - describedViewController.view.frame = view.bounds - describedViewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight] - view.addSubview(describedViewController.view) - describedViewController.didMove(toParent: self) - } - - override func preferredContentSizeDidChange(forChildContentContainer container: UIContentContainer) { - super.preferredContentSizeDidChange(forChildContentContainer: container) - - guard container === describedViewController else { return } - - sink.send(value: container.preferredContentSize) - } - } - - fileprivate class CounterViewController: UIViewController { - var count: Int { - didSet { - preferredContentSize.width = CGFloat(count * 10) - } - } - - init(count: Int) { - self.count = count - super.init(nibName: nil, bundle: nil) - preferredContentSize.width = CGFloat(count * 10) - } - - @available(*, unavailable) required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - } - - fileprivate class MessageViewController: UIViewController { - var message: String { - didSet { - preferredContentSize.width = CGFloat(message.count * 10) - } - } - - init(message: String) { - self.message = message - super.init(nibName: nil, bundle: nil) - preferredContentSize.width = CGFloat(message.count * 10) - } - - @available(*, unavailable) required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - } - -#endif diff --git a/swift/WorkflowUI/Tests/ViewControllerDescriptionTests.swift b/swift/WorkflowUI/Tests/ViewControllerDescriptionTests.swift deleted file mode 100644 index bf5331086..000000000 --- a/swift/WorkflowUI/Tests/ViewControllerDescriptionTests.swift +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright 2020 Square Inc. - * - * 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. - */ - -#if canImport(UIKit) - - import XCTest - - import ReactiveSwift - import Workflow - @testable import WorkflowUI - - fileprivate class BlankViewController: UIViewController {} - - class ViewControllerDescriptionTests: XCTestCase { - func test_build() { - let description = ViewControllerDescription( - build: { BlankViewController() }, - update: { _ in } - ) - - // Check built view controller - let viewController = description.buildViewController() - XCTAssertTrue(type(of: viewController) == BlankViewController.self) - - // Check another built view controller isn’t somehow the same instance - let viewControllerAgain = description.buildViewController() - XCTAssertFalse(viewController === viewControllerAgain) - } - - func test_canUpdate() { - let description = ViewControllerDescription( - build: { BlankViewController() }, - update: { _ in } - ) - - let viewController = description.buildViewController() - XCTAssertTrue(description.canUpdate(viewController: viewController)) - - let otherViewController = UIViewController() - XCTAssertFalse(description.canUpdate(viewController: otherViewController)) - - final class SubclassViewController: BlankViewController {} - - // We only update exact type matches, not subclasses - let subclassViewController = SubclassViewController() - XCTAssertFalse(description.canUpdate(viewController: subclassViewController)) - } - - func test_update() { - var updateCount = 0 - let description = ViewControllerDescription( - build: { BlankViewController() }, - update: { viewController in - XCTAssertTrue(type(of: viewController) == BlankViewController.self) - updateCount += 1 - } - ) - - XCTAssertEqual(updateCount, 0) - - // Build causes an initial update - let viewController = description.buildViewController() - XCTAssertEqual(updateCount, 1) - - description.update(viewController: viewController) - XCTAssertEqual(updateCount, 2) - - description.update(viewController: viewController) - XCTAssertEqual(updateCount, 3) - } - - func test_screenViewController() { - // Make sure ScreenViewController.description(for:) generates a correct view controller - // description - - struct MyScreen: Screen { - func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { - return MyScreenViewController.description(for: self, environment: environment) - } - } - - final class MyScreenViewController: ScreenViewController {} - - let screen = MyScreen() - let description = screen.viewControllerDescription(environment: .empty) - - let viewController = description.buildViewController() - XCTAssertTrue(type(of: viewController) == MyScreenViewController.self) - - XCTAssertTrue(description.canUpdate(viewController: viewController)) - - let viewControllerAgain = description.buildViewController() - XCTAssertFalse(viewController === viewControllerAgain) - } - } - -#endif diff --git a/kotlin/trace-encoder/README.md b/trace-encoder/README.md similarity index 100% rename from kotlin/trace-encoder/README.md rename to trace-encoder/README.md diff --git a/kotlin/trace-encoder/api/trace-encoder.api b/trace-encoder/api/trace-encoder.api similarity index 100% rename from kotlin/trace-encoder/api/trace-encoder.api rename to trace-encoder/api/trace-encoder.api diff --git a/kotlin/trace-encoder/build.gradle.kts b/trace-encoder/build.gradle.kts similarity index 100% rename from kotlin/trace-encoder/build.gradle.kts rename to trace-encoder/build.gradle.kts diff --git a/kotlin/trace-encoder/gradle.properties b/trace-encoder/gradle.properties similarity index 100% rename from kotlin/trace-encoder/gradle.properties rename to trace-encoder/gradle.properties diff --git a/kotlin/trace-encoder/src/main/java/com/squareup/tracing/ChromeTraceEvent.kt b/trace-encoder/src/main/java/com/squareup/tracing/ChromeTraceEvent.kt similarity index 100% rename from kotlin/trace-encoder/src/main/java/com/squareup/tracing/ChromeTraceEvent.kt rename to trace-encoder/src/main/java/com/squareup/tracing/ChromeTraceEvent.kt diff --git a/kotlin/trace-encoder/src/main/java/com/squareup/tracing/TraceEncoder.kt b/trace-encoder/src/main/java/com/squareup/tracing/TraceEncoder.kt similarity index 100% rename from kotlin/trace-encoder/src/main/java/com/squareup/tracing/TraceEncoder.kt rename to trace-encoder/src/main/java/com/squareup/tracing/TraceEncoder.kt diff --git a/kotlin/trace-encoder/src/main/java/com/squareup/tracing/TraceEvent.kt b/trace-encoder/src/main/java/com/squareup/tracing/TraceEvent.kt similarity index 100% rename from kotlin/trace-encoder/src/main/java/com/squareup/tracing/TraceEvent.kt rename to trace-encoder/src/main/java/com/squareup/tracing/TraceEvent.kt diff --git a/kotlin/trace-encoder/src/main/java/com/squareup/tracing/TraceLogger.kt b/trace-encoder/src/main/java/com/squareup/tracing/TraceLogger.kt similarity index 100% rename from kotlin/trace-encoder/src/main/java/com/squareup/tracing/TraceLogger.kt rename to trace-encoder/src/main/java/com/squareup/tracing/TraceLogger.kt diff --git a/kotlin/trace-encoder/src/test/java/com/squareup/tracing/ChromeTraceEventTest.kt b/trace-encoder/src/test/java/com/squareup/tracing/ChromeTraceEventTest.kt similarity index 100% rename from kotlin/trace-encoder/src/test/java/com/squareup/tracing/ChromeTraceEventTest.kt rename to trace-encoder/src/test/java/com/squareup/tracing/ChromeTraceEventTest.kt diff --git a/kotlin/trace-encoder/src/test/java/com/squareup/tracing/TraceEncoderTest.kt b/trace-encoder/src/test/java/com/squareup/tracing/TraceEncoderTest.kt similarity index 100% rename from kotlin/trace-encoder/src/test/java/com/squareup/tracing/TraceEncoderTest.kt rename to trace-encoder/src/test/java/com/squareup/tracing/TraceEncoderTest.kt diff --git a/kotlin/workflow-core/README.md b/workflow-core/README.md similarity index 100% rename from kotlin/workflow-core/README.md rename to workflow-core/README.md diff --git a/kotlin/workflow-core/api/workflow-core.api b/workflow-core/api/workflow-core.api similarity index 100% rename from kotlin/workflow-core/api/workflow-core.api rename to workflow-core/api/workflow-core.api diff --git a/kotlin/workflow-core/build.gradle.kts b/workflow-core/build.gradle.kts similarity index 100% rename from kotlin/workflow-core/build.gradle.kts rename to workflow-core/build.gradle.kts diff --git a/kotlin/workflow-core/gradle.properties b/workflow-core/gradle.properties similarity index 100% rename from kotlin/workflow-core/gradle.properties rename to workflow-core/gradle.properties diff --git a/kotlin/workflow-core/src/main/java/com/squareup/workflow/EventHandler.kt b/workflow-core/src/main/java/com/squareup/workflow/EventHandler.kt similarity index 100% rename from kotlin/workflow-core/src/main/java/com/squareup/workflow/EventHandler.kt rename to workflow-core/src/main/java/com/squareup/workflow/EventHandler.kt diff --git a/kotlin/workflow-core/src/main/java/com/squareup/workflow/LifecycleWorker.kt b/workflow-core/src/main/java/com/squareup/workflow/LifecycleWorker.kt similarity index 100% rename from kotlin/workflow-core/src/main/java/com/squareup/workflow/LifecycleWorker.kt rename to workflow-core/src/main/java/com/squareup/workflow/LifecycleWorker.kt diff --git a/kotlin/workflow-core/src/main/java/com/squareup/workflow/RenderContext.kt b/workflow-core/src/main/java/com/squareup/workflow/RenderContext.kt similarity index 100% rename from kotlin/workflow-core/src/main/java/com/squareup/workflow/RenderContext.kt rename to workflow-core/src/main/java/com/squareup/workflow/RenderContext.kt diff --git a/kotlin/workflow-core/src/main/java/com/squareup/workflow/Sink.kt b/workflow-core/src/main/java/com/squareup/workflow/Sink.kt similarity index 100% rename from kotlin/workflow-core/src/main/java/com/squareup/workflow/Sink.kt rename to workflow-core/src/main/java/com/squareup/workflow/Sink.kt diff --git a/kotlin/workflow-core/src/main/java/com/squareup/workflow/Snapshot.kt b/workflow-core/src/main/java/com/squareup/workflow/Snapshot.kt similarity index 100% rename from kotlin/workflow-core/src/main/java/com/squareup/workflow/Snapshot.kt rename to workflow-core/src/main/java/com/squareup/workflow/Snapshot.kt diff --git a/kotlin/workflow-core/src/main/java/com/squareup/workflow/StatefulWorkflow.kt b/workflow-core/src/main/java/com/squareup/workflow/StatefulWorkflow.kt similarity index 100% rename from kotlin/workflow-core/src/main/java/com/squareup/workflow/StatefulWorkflow.kt rename to workflow-core/src/main/java/com/squareup/workflow/StatefulWorkflow.kt diff --git a/kotlin/workflow-core/src/main/java/com/squareup/workflow/StatelessWorkflow.kt b/workflow-core/src/main/java/com/squareup/workflow/StatelessWorkflow.kt similarity index 100% rename from kotlin/workflow-core/src/main/java/com/squareup/workflow/StatelessWorkflow.kt rename to workflow-core/src/main/java/com/squareup/workflow/StatelessWorkflow.kt diff --git a/kotlin/workflow-core/src/main/java/com/squareup/workflow/VeryExperimentalWorkflow.kt b/workflow-core/src/main/java/com/squareup/workflow/VeryExperimentalWorkflow.kt similarity index 100% rename from kotlin/workflow-core/src/main/java/com/squareup/workflow/VeryExperimentalWorkflow.kt rename to workflow-core/src/main/java/com/squareup/workflow/VeryExperimentalWorkflow.kt diff --git a/kotlin/workflow-core/src/main/java/com/squareup/workflow/Worker.kt b/workflow-core/src/main/java/com/squareup/workflow/Worker.kt similarity index 100% rename from kotlin/workflow-core/src/main/java/com/squareup/workflow/Worker.kt rename to workflow-core/src/main/java/com/squareup/workflow/Worker.kt diff --git a/kotlin/workflow-core/src/main/java/com/squareup/workflow/Workflow.kt b/workflow-core/src/main/java/com/squareup/workflow/Workflow.kt similarity index 100% rename from kotlin/workflow-core/src/main/java/com/squareup/workflow/Workflow.kt rename to workflow-core/src/main/java/com/squareup/workflow/Workflow.kt diff --git a/kotlin/workflow-core/src/main/java/com/squareup/workflow/WorkflowAction.kt b/workflow-core/src/main/java/com/squareup/workflow/WorkflowAction.kt similarity index 100% rename from kotlin/workflow-core/src/main/java/com/squareup/workflow/WorkflowAction.kt rename to workflow-core/src/main/java/com/squareup/workflow/WorkflowAction.kt diff --git a/kotlin/workflow-core/src/test/java/com/squareup/workflow/WorkerTest.kt b/workflow-core/src/test/java/com/squareup/workflow/WorkerTest.kt similarity index 100% rename from kotlin/workflow-core/src/test/java/com/squareup/workflow/WorkerTest.kt rename to workflow-core/src/test/java/com/squareup/workflow/WorkerTest.kt diff --git a/kotlin/workflow-runtime/README.md b/workflow-runtime/README.md similarity index 100% rename from kotlin/workflow-runtime/README.md rename to workflow-runtime/README.md diff --git a/kotlin/workflow-runtime/api/workflow-runtime.api b/workflow-runtime/api/workflow-runtime.api similarity index 100% rename from kotlin/workflow-runtime/api/workflow-runtime.api rename to workflow-runtime/api/workflow-runtime.api diff --git a/kotlin/workflow-runtime/build.gradle.kts b/workflow-runtime/build.gradle.kts similarity index 100% rename from kotlin/workflow-runtime/build.gradle.kts rename to workflow-runtime/build.gradle.kts diff --git a/kotlin/workflow-runtime/gradle.properties b/workflow-runtime/gradle.properties similarity index 100% rename from kotlin/workflow-runtime/gradle.properties rename to workflow-runtime/gradle.properties diff --git a/kotlin/workflow-runtime/src/jmh/java/com/squareup/workflow/WorkflowNodeBenchmark.kt b/workflow-runtime/src/jmh/java/com/squareup/workflow/WorkflowNodeBenchmark.kt similarity index 100% rename from kotlin/workflow-runtime/src/jmh/java/com/squareup/workflow/WorkflowNodeBenchmark.kt rename to workflow-runtime/src/jmh/java/com/squareup/workflow/WorkflowNodeBenchmark.kt diff --git a/kotlin/workflow-runtime/src/main/java/com/squareup/workflow/LaunchWorkflow.kt b/workflow-runtime/src/main/java/com/squareup/workflow/LaunchWorkflow.kt similarity index 100% rename from kotlin/workflow-runtime/src/main/java/com/squareup/workflow/LaunchWorkflow.kt rename to workflow-runtime/src/main/java/com/squareup/workflow/LaunchWorkflow.kt diff --git a/kotlin/workflow-runtime/src/main/java/com/squareup/workflow/RenderingAndSnapshot.kt b/workflow-runtime/src/main/java/com/squareup/workflow/RenderingAndSnapshot.kt similarity index 100% rename from kotlin/workflow-runtime/src/main/java/com/squareup/workflow/RenderingAndSnapshot.kt rename to workflow-runtime/src/main/java/com/squareup/workflow/RenderingAndSnapshot.kt diff --git a/kotlin/workflow-runtime/src/main/java/com/squareup/workflow/WorkflowSession.kt b/workflow-runtime/src/main/java/com/squareup/workflow/WorkflowSession.kt similarity index 100% rename from kotlin/workflow-runtime/src/main/java/com/squareup/workflow/WorkflowSession.kt rename to workflow-runtime/src/main/java/com/squareup/workflow/WorkflowSession.kt diff --git a/kotlin/workflow-runtime/src/main/java/com/squareup/workflow/diagnostic/ChainedDiagnosticListener.kt b/workflow-runtime/src/main/java/com/squareup/workflow/diagnostic/ChainedDiagnosticListener.kt similarity index 100% rename from kotlin/workflow-runtime/src/main/java/com/squareup/workflow/diagnostic/ChainedDiagnosticListener.kt rename to workflow-runtime/src/main/java/com/squareup/workflow/diagnostic/ChainedDiagnosticListener.kt diff --git a/kotlin/workflow-runtime/src/main/java/com/squareup/workflow/diagnostic/DebugSnapshotRecordingListener.kt b/workflow-runtime/src/main/java/com/squareup/workflow/diagnostic/DebugSnapshotRecordingListener.kt similarity index 100% rename from kotlin/workflow-runtime/src/main/java/com/squareup/workflow/diagnostic/DebugSnapshotRecordingListener.kt rename to workflow-runtime/src/main/java/com/squareup/workflow/diagnostic/DebugSnapshotRecordingListener.kt diff --git a/kotlin/workflow-runtime/src/main/java/com/squareup/workflow/diagnostic/IdCounter.kt b/workflow-runtime/src/main/java/com/squareup/workflow/diagnostic/IdCounter.kt similarity index 100% rename from kotlin/workflow-runtime/src/main/java/com/squareup/workflow/diagnostic/IdCounter.kt rename to workflow-runtime/src/main/java/com/squareup/workflow/diagnostic/IdCounter.kt diff --git a/kotlin/workflow-runtime/src/main/java/com/squareup/workflow/diagnostic/SimpleLoggingDiagnosticListener.kt b/workflow-runtime/src/main/java/com/squareup/workflow/diagnostic/SimpleLoggingDiagnosticListener.kt similarity index 100% rename from kotlin/workflow-runtime/src/main/java/com/squareup/workflow/diagnostic/SimpleLoggingDiagnosticListener.kt rename to workflow-runtime/src/main/java/com/squareup/workflow/diagnostic/SimpleLoggingDiagnosticListener.kt diff --git a/kotlin/workflow-runtime/src/main/java/com/squareup/workflow/diagnostic/WorkflowDiagnosticListener.kt b/workflow-runtime/src/main/java/com/squareup/workflow/diagnostic/WorkflowDiagnosticListener.kt similarity index 100% rename from kotlin/workflow-runtime/src/main/java/com/squareup/workflow/diagnostic/WorkflowDiagnosticListener.kt rename to workflow-runtime/src/main/java/com/squareup/workflow/diagnostic/WorkflowDiagnosticListener.kt diff --git a/kotlin/workflow-runtime/src/main/java/com/squareup/workflow/diagnostic/WorkflowHierarchyDebugSnapshot.kt b/workflow-runtime/src/main/java/com/squareup/workflow/diagnostic/WorkflowHierarchyDebugSnapshot.kt similarity index 100% rename from kotlin/workflow-runtime/src/main/java/com/squareup/workflow/diagnostic/WorkflowHierarchyDebugSnapshot.kt rename to workflow-runtime/src/main/java/com/squareup/workflow/diagnostic/WorkflowHierarchyDebugSnapshot.kt diff --git a/kotlin/workflow-runtime/src/main/java/com/squareup/workflow/diagnostic/WorkflowUpdateDebugInfo.kt b/workflow-runtime/src/main/java/com/squareup/workflow/diagnostic/WorkflowUpdateDebugInfo.kt similarity index 100% rename from kotlin/workflow-runtime/src/main/java/com/squareup/workflow/diagnostic/WorkflowUpdateDebugInfo.kt rename to workflow-runtime/src/main/java/com/squareup/workflow/diagnostic/WorkflowUpdateDebugInfo.kt diff --git a/kotlin/workflow-runtime/src/main/java/com/squareup/workflow/internal/ActiveStagingList.kt b/workflow-runtime/src/main/java/com/squareup/workflow/internal/ActiveStagingList.kt similarity index 100% rename from kotlin/workflow-runtime/src/main/java/com/squareup/workflow/internal/ActiveStagingList.kt rename to workflow-runtime/src/main/java/com/squareup/workflow/internal/ActiveStagingList.kt diff --git a/kotlin/workflow-runtime/src/main/java/com/squareup/workflow/internal/InlineLinkedList.kt b/workflow-runtime/src/main/java/com/squareup/workflow/internal/InlineLinkedList.kt similarity index 100% rename from kotlin/workflow-runtime/src/main/java/com/squareup/workflow/internal/InlineLinkedList.kt rename to workflow-runtime/src/main/java/com/squareup/workflow/internal/InlineLinkedList.kt diff --git a/kotlin/workflow-runtime/src/main/java/com/squareup/workflow/internal/RealRenderContext.kt b/workflow-runtime/src/main/java/com/squareup/workflow/internal/RealRenderContext.kt similarity index 100% rename from kotlin/workflow-runtime/src/main/java/com/squareup/workflow/internal/RealRenderContext.kt rename to workflow-runtime/src/main/java/com/squareup/workflow/internal/RealRenderContext.kt diff --git a/kotlin/workflow-runtime/src/main/java/com/squareup/workflow/internal/SubtreeManager.kt b/workflow-runtime/src/main/java/com/squareup/workflow/internal/SubtreeManager.kt similarity index 100% rename from kotlin/workflow-runtime/src/main/java/com/squareup/workflow/internal/SubtreeManager.kt rename to workflow-runtime/src/main/java/com/squareup/workflow/internal/SubtreeManager.kt diff --git a/kotlin/workflow-runtime/src/main/java/com/squareup/workflow/internal/Throwables.kt b/workflow-runtime/src/main/java/com/squareup/workflow/internal/Throwables.kt similarity index 100% rename from kotlin/workflow-runtime/src/main/java/com/squareup/workflow/internal/Throwables.kt rename to workflow-runtime/src/main/java/com/squareup/workflow/internal/Throwables.kt diff --git a/kotlin/workflow-runtime/src/main/java/com/squareup/workflow/internal/TreeSnapshots.kt b/workflow-runtime/src/main/java/com/squareup/workflow/internal/TreeSnapshots.kt similarity index 100% rename from kotlin/workflow-runtime/src/main/java/com/squareup/workflow/internal/TreeSnapshots.kt rename to workflow-runtime/src/main/java/com/squareup/workflow/internal/TreeSnapshots.kt diff --git a/kotlin/workflow-runtime/src/main/java/com/squareup/workflow/internal/WorkerChildNode.kt b/workflow-runtime/src/main/java/com/squareup/workflow/internal/WorkerChildNode.kt similarity index 100% rename from kotlin/workflow-runtime/src/main/java/com/squareup/workflow/internal/WorkerChildNode.kt rename to workflow-runtime/src/main/java/com/squareup/workflow/internal/WorkerChildNode.kt diff --git a/kotlin/workflow-runtime/src/main/java/com/squareup/workflow/internal/Workers.kt b/workflow-runtime/src/main/java/com/squareup/workflow/internal/Workers.kt similarity index 100% rename from kotlin/workflow-runtime/src/main/java/com/squareup/workflow/internal/Workers.kt rename to workflow-runtime/src/main/java/com/squareup/workflow/internal/Workers.kt diff --git a/kotlin/workflow-runtime/src/main/java/com/squareup/workflow/internal/WorkflowChildNode.kt b/workflow-runtime/src/main/java/com/squareup/workflow/internal/WorkflowChildNode.kt similarity index 100% rename from kotlin/workflow-runtime/src/main/java/com/squareup/workflow/internal/WorkflowChildNode.kt rename to workflow-runtime/src/main/java/com/squareup/workflow/internal/WorkflowChildNode.kt diff --git a/kotlin/workflow-runtime/src/main/java/com/squareup/workflow/internal/WorkflowId.kt b/workflow-runtime/src/main/java/com/squareup/workflow/internal/WorkflowId.kt similarity index 100% rename from kotlin/workflow-runtime/src/main/java/com/squareup/workflow/internal/WorkflowId.kt rename to workflow-runtime/src/main/java/com/squareup/workflow/internal/WorkflowId.kt diff --git a/kotlin/workflow-runtime/src/main/java/com/squareup/workflow/internal/WorkflowLoop.kt b/workflow-runtime/src/main/java/com/squareup/workflow/internal/WorkflowLoop.kt similarity index 100% rename from kotlin/workflow-runtime/src/main/java/com/squareup/workflow/internal/WorkflowLoop.kt rename to workflow-runtime/src/main/java/com/squareup/workflow/internal/WorkflowLoop.kt diff --git a/kotlin/workflow-runtime/src/main/java/com/squareup/workflow/internal/WorkflowNode.kt b/workflow-runtime/src/main/java/com/squareup/workflow/internal/WorkflowNode.kt similarity index 100% rename from kotlin/workflow-runtime/src/main/java/com/squareup/workflow/internal/WorkflowNode.kt rename to workflow-runtime/src/main/java/com/squareup/workflow/internal/WorkflowNode.kt diff --git a/kotlin/workflow-runtime/src/main/java/com/squareup/workflow/internal/WorkflowRunner.kt b/workflow-runtime/src/main/java/com/squareup/workflow/internal/WorkflowRunner.kt similarity index 100% rename from kotlin/workflow-runtime/src/main/java/com/squareup/workflow/internal/WorkflowRunner.kt rename to workflow-runtime/src/main/java/com/squareup/workflow/internal/WorkflowRunner.kt diff --git a/kotlin/workflow-runtime/src/main/java/com/squareup/workflow/testing/LaunchWorkflow.kt b/workflow-runtime/src/main/java/com/squareup/workflow/testing/LaunchWorkflow.kt similarity index 100% rename from kotlin/workflow-runtime/src/main/java/com/squareup/workflow/testing/LaunchWorkflow.kt rename to workflow-runtime/src/main/java/com/squareup/workflow/testing/LaunchWorkflow.kt diff --git a/kotlin/workflow-runtime/src/main/java/com/squareup/workflow/testing/WorkflowTestParams.kt b/workflow-runtime/src/main/java/com/squareup/workflow/testing/WorkflowTestParams.kt similarity index 100% rename from kotlin/workflow-runtime/src/main/java/com/squareup/workflow/testing/WorkflowTestParams.kt rename to workflow-runtime/src/main/java/com/squareup/workflow/testing/WorkflowTestParams.kt diff --git a/kotlin/workflow-runtime/src/test/java/com/squareup/workflow/RenderWorkflowInTest.kt b/workflow-runtime/src/test/java/com/squareup/workflow/RenderWorkflowInTest.kt similarity index 100% rename from kotlin/workflow-runtime/src/test/java/com/squareup/workflow/RenderWorkflowInTest.kt rename to workflow-runtime/src/test/java/com/squareup/workflow/RenderWorkflowInTest.kt diff --git a/kotlin/workflow-runtime/src/test/java/com/squareup/workflow/diagnostic/AssertOverridesAllMethods.kt b/workflow-runtime/src/test/java/com/squareup/workflow/diagnostic/AssertOverridesAllMethods.kt similarity index 100% rename from kotlin/workflow-runtime/src/test/java/com/squareup/workflow/diagnostic/AssertOverridesAllMethods.kt rename to workflow-runtime/src/test/java/com/squareup/workflow/diagnostic/AssertOverridesAllMethods.kt diff --git a/kotlin/workflow-runtime/src/test/java/com/squareup/workflow/diagnostic/ChainedDiagnosticListenerTest.kt b/workflow-runtime/src/test/java/com/squareup/workflow/diagnostic/ChainedDiagnosticListenerTest.kt similarity index 100% rename from kotlin/workflow-runtime/src/test/java/com/squareup/workflow/diagnostic/ChainedDiagnosticListenerTest.kt rename to workflow-runtime/src/test/java/com/squareup/workflow/diagnostic/ChainedDiagnosticListenerTest.kt diff --git a/kotlin/workflow-runtime/src/test/java/com/squareup/workflow/diagnostic/DebugSnapshotRecordingListenerTest.kt b/workflow-runtime/src/test/java/com/squareup/workflow/diagnostic/DebugSnapshotRecordingListenerTest.kt similarity index 100% rename from kotlin/workflow-runtime/src/test/java/com/squareup/workflow/diagnostic/DebugSnapshotRecordingListenerTest.kt rename to workflow-runtime/src/test/java/com/squareup/workflow/diagnostic/DebugSnapshotRecordingListenerTest.kt diff --git a/kotlin/workflow-runtime/src/test/java/com/squareup/workflow/diagnostic/SimpleLoggingDiagnosticListenerTest.kt b/workflow-runtime/src/test/java/com/squareup/workflow/diagnostic/SimpleLoggingDiagnosticListenerTest.kt similarity index 100% rename from kotlin/workflow-runtime/src/test/java/com/squareup/workflow/diagnostic/SimpleLoggingDiagnosticListenerTest.kt rename to workflow-runtime/src/test/java/com/squareup/workflow/diagnostic/SimpleLoggingDiagnosticListenerTest.kt diff --git a/kotlin/workflow-runtime/src/test/java/com/squareup/workflow/diagnostic/WorkflowHierarchyDebugSnapshotTest.kt b/workflow-runtime/src/test/java/com/squareup/workflow/diagnostic/WorkflowHierarchyDebugSnapshotTest.kt similarity index 100% rename from kotlin/workflow-runtime/src/test/java/com/squareup/workflow/diagnostic/WorkflowHierarchyDebugSnapshotTest.kt rename to workflow-runtime/src/test/java/com/squareup/workflow/diagnostic/WorkflowHierarchyDebugSnapshotTest.kt diff --git a/kotlin/workflow-runtime/src/test/java/com/squareup/workflow/diagnostic/WorkflowUpdateDebugInfoTest.kt b/workflow-runtime/src/test/java/com/squareup/workflow/diagnostic/WorkflowUpdateDebugInfoTest.kt similarity index 100% rename from kotlin/workflow-runtime/src/test/java/com/squareup/workflow/diagnostic/WorkflowUpdateDebugInfoTest.kt rename to workflow-runtime/src/test/java/com/squareup/workflow/diagnostic/WorkflowUpdateDebugInfoTest.kt diff --git a/kotlin/workflow-runtime/src/test/java/com/squareup/workflow/internal/ActiveStagingListTest.kt b/workflow-runtime/src/test/java/com/squareup/workflow/internal/ActiveStagingListTest.kt similarity index 100% rename from kotlin/workflow-runtime/src/test/java/com/squareup/workflow/internal/ActiveStagingListTest.kt rename to workflow-runtime/src/test/java/com/squareup/workflow/internal/ActiveStagingListTest.kt diff --git a/kotlin/workflow-runtime/src/test/java/com/squareup/workflow/internal/InlineLinkedListTest.kt b/workflow-runtime/src/test/java/com/squareup/workflow/internal/InlineLinkedListTest.kt similarity index 100% rename from kotlin/workflow-runtime/src/test/java/com/squareup/workflow/internal/InlineLinkedListTest.kt rename to workflow-runtime/src/test/java/com/squareup/workflow/internal/InlineLinkedListTest.kt diff --git a/kotlin/workflow-runtime/src/test/java/com/squareup/workflow/internal/LaunchWorkflowTest.kt b/workflow-runtime/src/test/java/com/squareup/workflow/internal/LaunchWorkflowTest.kt similarity index 100% rename from kotlin/workflow-runtime/src/test/java/com/squareup/workflow/internal/LaunchWorkflowTest.kt rename to workflow-runtime/src/test/java/com/squareup/workflow/internal/LaunchWorkflowTest.kt diff --git a/kotlin/workflow-runtime/src/test/java/com/squareup/workflow/internal/NullFlowWorker.java b/workflow-runtime/src/test/java/com/squareup/workflow/internal/NullFlowWorker.java similarity index 100% rename from kotlin/workflow-runtime/src/test/java/com/squareup/workflow/internal/NullFlowWorker.java rename to workflow-runtime/src/test/java/com/squareup/workflow/internal/NullFlowWorker.java diff --git a/kotlin/workflow-runtime/src/test/java/com/squareup/workflow/internal/RealRenderContextTest.kt b/workflow-runtime/src/test/java/com/squareup/workflow/internal/RealRenderContextTest.kt similarity index 100% rename from kotlin/workflow-runtime/src/test/java/com/squareup/workflow/internal/RealRenderContextTest.kt rename to workflow-runtime/src/test/java/com/squareup/workflow/internal/RealRenderContextTest.kt diff --git a/kotlin/workflow-runtime/src/test/java/com/squareup/workflow/internal/RecordingDiagnosticListener.kt b/workflow-runtime/src/test/java/com/squareup/workflow/internal/RecordingDiagnosticListener.kt similarity index 100% rename from kotlin/workflow-runtime/src/test/java/com/squareup/workflow/internal/RecordingDiagnosticListener.kt rename to workflow-runtime/src/test/java/com/squareup/workflow/internal/RecordingDiagnosticListener.kt diff --git a/kotlin/workflow-runtime/src/test/java/com/squareup/workflow/internal/SubtreeManagerTest.kt b/workflow-runtime/src/test/java/com/squareup/workflow/internal/SubtreeManagerTest.kt similarity index 100% rename from kotlin/workflow-runtime/src/test/java/com/squareup/workflow/internal/SubtreeManagerTest.kt rename to workflow-runtime/src/test/java/com/squareup/workflow/internal/SubtreeManagerTest.kt diff --git a/kotlin/workflow-runtime/src/test/java/com/squareup/workflow/internal/TreeSnapshotsTest.kt b/workflow-runtime/src/test/java/com/squareup/workflow/internal/TreeSnapshotsTest.kt similarity index 100% rename from kotlin/workflow-runtime/src/test/java/com/squareup/workflow/internal/TreeSnapshotsTest.kt rename to workflow-runtime/src/test/java/com/squareup/workflow/internal/TreeSnapshotsTest.kt diff --git a/kotlin/workflow-runtime/src/test/java/com/squareup/workflow/internal/WorkersTest.kt b/workflow-runtime/src/test/java/com/squareup/workflow/internal/WorkersTest.kt similarity index 100% rename from kotlin/workflow-runtime/src/test/java/com/squareup/workflow/internal/WorkersTest.kt rename to workflow-runtime/src/test/java/com/squareup/workflow/internal/WorkersTest.kt diff --git a/kotlin/workflow-runtime/src/test/java/com/squareup/workflow/internal/WorkflowDiagnosticListenerIntegrationTest.kt b/workflow-runtime/src/test/java/com/squareup/workflow/internal/WorkflowDiagnosticListenerIntegrationTest.kt similarity index 100% rename from kotlin/workflow-runtime/src/test/java/com/squareup/workflow/internal/WorkflowDiagnosticListenerIntegrationTest.kt rename to workflow-runtime/src/test/java/com/squareup/workflow/internal/WorkflowDiagnosticListenerIntegrationTest.kt diff --git a/kotlin/workflow-runtime/src/test/java/com/squareup/workflow/internal/WorkflowDiagnosticListenerLegacyIntegrationTest.kt b/workflow-runtime/src/test/java/com/squareup/workflow/internal/WorkflowDiagnosticListenerLegacyIntegrationTest.kt similarity index 100% rename from kotlin/workflow-runtime/src/test/java/com/squareup/workflow/internal/WorkflowDiagnosticListenerLegacyIntegrationTest.kt rename to workflow-runtime/src/test/java/com/squareup/workflow/internal/WorkflowDiagnosticListenerLegacyIntegrationTest.kt diff --git a/kotlin/workflow-runtime/src/test/java/com/squareup/workflow/internal/WorkflowNodeTest.kt b/workflow-runtime/src/test/java/com/squareup/workflow/internal/WorkflowNodeTest.kt similarity index 100% rename from kotlin/workflow-runtime/src/test/java/com/squareup/workflow/internal/WorkflowNodeTest.kt rename to workflow-runtime/src/test/java/com/squareup/workflow/internal/WorkflowNodeTest.kt diff --git a/kotlin/workflow-runtime/src/test/java/com/squareup/workflow/internal/WorkflowRunnerTest.kt b/workflow-runtime/src/test/java/com/squareup/workflow/internal/WorkflowRunnerTest.kt similarity index 100% rename from kotlin/workflow-runtime/src/test/java/com/squareup/workflow/internal/WorkflowRunnerTest.kt rename to workflow-runtime/src/test/java/com/squareup/workflow/internal/WorkflowRunnerTest.kt diff --git a/kotlin/workflow-rx2/README.md b/workflow-rx2/README.md similarity index 100% rename from kotlin/workflow-rx2/README.md rename to workflow-rx2/README.md diff --git a/kotlin/workflow-rx2/api/workflow-rx2.api b/workflow-rx2/api/workflow-rx2.api similarity index 100% rename from kotlin/workflow-rx2/api/workflow-rx2.api rename to workflow-rx2/api/workflow-rx2.api diff --git a/kotlin/workflow-rx2/build.gradle.kts b/workflow-rx2/build.gradle.kts similarity index 100% rename from kotlin/workflow-rx2/build.gradle.kts rename to workflow-rx2/build.gradle.kts diff --git a/kotlin/workflow-rx2/gradle.properties b/workflow-rx2/gradle.properties similarity index 100% rename from kotlin/workflow-rx2/gradle.properties rename to workflow-rx2/gradle.properties diff --git a/kotlin/workflow-rx2/src/main/java/com/squareup/workflow/rx2/PublisherWorker.kt b/workflow-rx2/src/main/java/com/squareup/workflow/rx2/PublisherWorker.kt similarity index 100% rename from kotlin/workflow-rx2/src/main/java/com/squareup/workflow/rx2/PublisherWorker.kt rename to workflow-rx2/src/main/java/com/squareup/workflow/rx2/PublisherWorker.kt diff --git a/kotlin/workflow-rx2/src/main/java/com/squareup/workflow/rx2/RxWorkers.kt b/workflow-rx2/src/main/java/com/squareup/workflow/rx2/RxWorkers.kt similarity index 100% rename from kotlin/workflow-rx2/src/main/java/com/squareup/workflow/rx2/RxWorkers.kt rename to workflow-rx2/src/main/java/com/squareup/workflow/rx2/RxWorkers.kt diff --git a/kotlin/workflow-rx2/src/test/java/com/squareup/workflow/rx2/PublisherWorkerTest.kt b/workflow-rx2/src/test/java/com/squareup/workflow/rx2/PublisherWorkerTest.kt similarity index 100% rename from kotlin/workflow-rx2/src/test/java/com/squareup/workflow/rx2/PublisherWorkerTest.kt rename to workflow-rx2/src/test/java/com/squareup/workflow/rx2/PublisherWorkerTest.kt diff --git a/kotlin/workflow-rx2/src/test/java/com/squareup/workflow/rx2/RxWorkersTest.kt b/workflow-rx2/src/test/java/com/squareup/workflow/rx2/RxWorkersTest.kt similarity index 100% rename from kotlin/workflow-rx2/src/test/java/com/squareup/workflow/rx2/RxWorkersTest.kt rename to workflow-rx2/src/test/java/com/squareup/workflow/rx2/RxWorkersTest.kt diff --git a/kotlin/workflow-testing/README.md b/workflow-testing/README.md similarity index 100% rename from kotlin/workflow-testing/README.md rename to workflow-testing/README.md diff --git a/kotlin/workflow-testing/api/workflow-testing.api b/workflow-testing/api/workflow-testing.api similarity index 100% rename from kotlin/workflow-testing/api/workflow-testing.api rename to workflow-testing/api/workflow-testing.api diff --git a/kotlin/workflow-testing/build.gradle.kts b/workflow-testing/build.gradle.kts similarity index 100% rename from kotlin/workflow-testing/build.gradle.kts rename to workflow-testing/build.gradle.kts diff --git a/kotlin/workflow-testing/gradle.properties b/workflow-testing/gradle.properties similarity index 100% rename from kotlin/workflow-testing/gradle.properties rename to workflow-testing/gradle.properties diff --git a/kotlin/workflow-testing/src/main/java/com/squareup/workflow/testing/RealRenderTester.kt b/workflow-testing/src/main/java/com/squareup/workflow/testing/RealRenderTester.kt similarity index 100% rename from kotlin/workflow-testing/src/main/java/com/squareup/workflow/testing/RealRenderTester.kt rename to workflow-testing/src/main/java/com/squareup/workflow/testing/RealRenderTester.kt diff --git a/kotlin/workflow-testing/src/main/java/com/squareup/workflow/testing/RenderTestResult.kt b/workflow-testing/src/main/java/com/squareup/workflow/testing/RenderTestResult.kt similarity index 100% rename from kotlin/workflow-testing/src/main/java/com/squareup/workflow/testing/RenderTestResult.kt rename to workflow-testing/src/main/java/com/squareup/workflow/testing/RenderTestResult.kt diff --git a/kotlin/workflow-testing/src/main/java/com/squareup/workflow/testing/RenderTester.kt b/workflow-testing/src/main/java/com/squareup/workflow/testing/RenderTester.kt similarity index 100% rename from kotlin/workflow-testing/src/main/java/com/squareup/workflow/testing/RenderTester.kt rename to workflow-testing/src/main/java/com/squareup/workflow/testing/RenderTester.kt diff --git a/kotlin/workflow-testing/src/main/java/com/squareup/workflow/testing/WorkerSink.kt b/workflow-testing/src/main/java/com/squareup/workflow/testing/WorkerSink.kt similarity index 100% rename from kotlin/workflow-testing/src/main/java/com/squareup/workflow/testing/WorkerSink.kt rename to workflow-testing/src/main/java/com/squareup/workflow/testing/WorkerSink.kt diff --git a/kotlin/workflow-testing/src/main/java/com/squareup/workflow/testing/WorkerTester.kt b/workflow-testing/src/main/java/com/squareup/workflow/testing/WorkerTester.kt similarity index 100% rename from kotlin/workflow-testing/src/main/java/com/squareup/workflow/testing/WorkerTester.kt rename to workflow-testing/src/main/java/com/squareup/workflow/testing/WorkerTester.kt diff --git a/kotlin/workflow-testing/src/main/java/com/squareup/workflow/testing/WorkflowTester.kt b/workflow-testing/src/main/java/com/squareup/workflow/testing/WorkflowTester.kt similarity index 100% rename from kotlin/workflow-testing/src/main/java/com/squareup/workflow/testing/WorkflowTester.kt rename to workflow-testing/src/main/java/com/squareup/workflow/testing/WorkflowTester.kt diff --git a/kotlin/workflow-testing/src/test/java/com/squareup/workflow/FlowWorkersTest.kt b/workflow-testing/src/test/java/com/squareup/workflow/FlowWorkersTest.kt similarity index 100% rename from kotlin/workflow-testing/src/test/java/com/squareup/workflow/FlowWorkersTest.kt rename to workflow-testing/src/test/java/com/squareup/workflow/FlowWorkersTest.kt diff --git a/kotlin/workflow-testing/src/test/java/com/squareup/workflow/LifecycleWorkerTest.kt b/workflow-testing/src/test/java/com/squareup/workflow/LifecycleWorkerTest.kt similarity index 100% rename from kotlin/workflow-testing/src/test/java/com/squareup/workflow/LifecycleWorkerTest.kt rename to workflow-testing/src/test/java/com/squareup/workflow/LifecycleWorkerTest.kt diff --git a/kotlin/workflow-testing/src/test/java/com/squareup/workflow/SnapshottingIntegrationTest.kt b/workflow-testing/src/test/java/com/squareup/workflow/SnapshottingIntegrationTest.kt similarity index 100% rename from kotlin/workflow-testing/src/test/java/com/squareup/workflow/SnapshottingIntegrationTest.kt rename to workflow-testing/src/test/java/com/squareup/workflow/SnapshottingIntegrationTest.kt diff --git a/kotlin/workflow-testing/src/test/java/com/squareup/workflow/TreeWorkflow.kt b/workflow-testing/src/test/java/com/squareup/workflow/TreeWorkflow.kt similarity index 100% rename from kotlin/workflow-testing/src/test/java/com/squareup/workflow/TreeWorkflow.kt rename to workflow-testing/src/test/java/com/squareup/workflow/TreeWorkflow.kt diff --git a/kotlin/workflow-testing/src/test/java/com/squareup/workflow/WorkerCompositionIntegrationTest.kt b/workflow-testing/src/test/java/com/squareup/workflow/WorkerCompositionIntegrationTest.kt similarity index 100% rename from kotlin/workflow-testing/src/test/java/com/squareup/workflow/WorkerCompositionIntegrationTest.kt rename to workflow-testing/src/test/java/com/squareup/workflow/WorkerCompositionIntegrationTest.kt diff --git a/kotlin/workflow-testing/src/test/java/com/squareup/workflow/WorkerStressTest.kt b/workflow-testing/src/test/java/com/squareup/workflow/WorkerStressTest.kt similarity index 100% rename from kotlin/workflow-testing/src/test/java/com/squareup/workflow/WorkerStressTest.kt rename to workflow-testing/src/test/java/com/squareup/workflow/WorkerStressTest.kt diff --git a/kotlin/workflow-testing/src/test/java/com/squareup/workflow/WorkerTest.kt b/workflow-testing/src/test/java/com/squareup/workflow/WorkerTest.kt similarity index 100% rename from kotlin/workflow-testing/src/test/java/com/squareup/workflow/WorkerTest.kt rename to workflow-testing/src/test/java/com/squareup/workflow/WorkerTest.kt diff --git a/kotlin/workflow-testing/src/test/java/com/squareup/workflow/WorkflowCompositionIntegrationTest.kt b/workflow-testing/src/test/java/com/squareup/workflow/WorkflowCompositionIntegrationTest.kt similarity index 100% rename from kotlin/workflow-testing/src/test/java/com/squareup/workflow/WorkflowCompositionIntegrationTest.kt rename to workflow-testing/src/test/java/com/squareup/workflow/WorkflowCompositionIntegrationTest.kt diff --git a/kotlin/workflow-testing/src/test/java/com/squareup/workflow/testing/RealRenderTesterTest.kt b/workflow-testing/src/test/java/com/squareup/workflow/testing/RealRenderTesterTest.kt similarity index 100% rename from kotlin/workflow-testing/src/test/java/com/squareup/workflow/testing/RealRenderTesterTest.kt rename to workflow-testing/src/test/java/com/squareup/workflow/testing/RealRenderTesterTest.kt diff --git a/kotlin/workflow-testing/src/test/java/com/squareup/workflow/testing/WorkerSinkTest.kt b/workflow-testing/src/test/java/com/squareup/workflow/testing/WorkerSinkTest.kt similarity index 100% rename from kotlin/workflow-testing/src/test/java/com/squareup/workflow/testing/WorkerSinkTest.kt rename to workflow-testing/src/test/java/com/squareup/workflow/testing/WorkerSinkTest.kt diff --git a/kotlin/workflow-testing/src/test/java/com/squareup/workflow/testing/WorkflowTesterTest.kt b/workflow-testing/src/test/java/com/squareup/workflow/testing/WorkflowTesterTest.kt similarity index 100% rename from kotlin/workflow-testing/src/test/java/com/squareup/workflow/testing/WorkflowTesterTest.kt rename to workflow-testing/src/test/java/com/squareup/workflow/testing/WorkflowTesterTest.kt diff --git a/kotlin/workflow-tracing/README.md b/workflow-tracing/README.md similarity index 100% rename from kotlin/workflow-tracing/README.md rename to workflow-tracing/README.md diff --git a/kotlin/workflow-tracing/api/workflow-tracing.api b/workflow-tracing/api/workflow-tracing.api similarity index 100% rename from kotlin/workflow-tracing/api/workflow-tracing.api rename to workflow-tracing/api/workflow-tracing.api diff --git a/kotlin/workflow-tracing/build.gradle.kts b/workflow-tracing/build.gradle.kts similarity index 100% rename from kotlin/workflow-tracing/build.gradle.kts rename to workflow-tracing/build.gradle.kts diff --git a/kotlin/workflow-tracing/gradle.properties b/workflow-tracing/gradle.properties similarity index 100% rename from kotlin/workflow-tracing/gradle.properties rename to workflow-tracing/gradle.properties diff --git a/kotlin/workflow-tracing/src/main/java/com/squareup/workflow/diagnostic/tracing/GcDetector.kt b/workflow-tracing/src/main/java/com/squareup/workflow/diagnostic/tracing/GcDetector.kt similarity index 100% rename from kotlin/workflow-tracing/src/main/java/com/squareup/workflow/diagnostic/tracing/GcDetector.kt rename to workflow-tracing/src/main/java/com/squareup/workflow/diagnostic/tracing/GcDetector.kt diff --git a/kotlin/workflow-tracing/src/main/java/com/squareup/workflow/diagnostic/tracing/MemoryStats.kt b/workflow-tracing/src/main/java/com/squareup/workflow/diagnostic/tracing/MemoryStats.kt similarity index 100% rename from kotlin/workflow-tracing/src/main/java/com/squareup/workflow/diagnostic/tracing/MemoryStats.kt rename to workflow-tracing/src/main/java/com/squareup/workflow/diagnostic/tracing/MemoryStats.kt diff --git a/kotlin/workflow-tracing/src/main/java/com/squareup/workflow/diagnostic/tracing/TracingDiagnosticListener.kt b/workflow-tracing/src/main/java/com/squareup/workflow/diagnostic/tracing/TracingDiagnosticListener.kt similarity index 100% rename from kotlin/workflow-tracing/src/main/java/com/squareup/workflow/diagnostic/tracing/TracingDiagnosticListener.kt rename to workflow-tracing/src/main/java/com/squareup/workflow/diagnostic/tracing/TracingDiagnosticListener.kt diff --git a/kotlin/workflow-tracing/src/test/java/com/squareup/workflow/diagnostic/tracing/TracingDiagnosticListenerTest.kt b/workflow-tracing/src/test/java/com/squareup/workflow/diagnostic/tracing/TracingDiagnosticListenerTest.kt similarity index 100% rename from kotlin/workflow-tracing/src/test/java/com/squareup/workflow/diagnostic/tracing/TracingDiagnosticListenerTest.kt rename to workflow-tracing/src/test/java/com/squareup/workflow/diagnostic/tracing/TracingDiagnosticListenerTest.kt diff --git a/kotlin/workflow-tracing/src/test/resources/com/squareup/workflow/diagnostic/tracing/expected_trace_file.txt b/workflow-tracing/src/test/resources/com/squareup/workflow/diagnostic/tracing/expected_trace_file.txt similarity index 100% rename from kotlin/workflow-tracing/src/test/resources/com/squareup/workflow/diagnostic/tracing/expected_trace_file.txt rename to workflow-tracing/src/test/resources/com/squareup/workflow/diagnostic/tracing/expected_trace_file.txt diff --git a/kotlin/workflow-ui/backstack-android/api/backstack-android.api b/workflow-ui/backstack-android/api/backstack-android.api similarity index 100% rename from kotlin/workflow-ui/backstack-android/api/backstack-android.api rename to workflow-ui/backstack-android/api/backstack-android.api diff --git a/kotlin/workflow-ui/backstack-android/build.gradle.kts b/workflow-ui/backstack-android/build.gradle.kts similarity index 100% rename from kotlin/workflow-ui/backstack-android/build.gradle.kts rename to workflow-ui/backstack-android/build.gradle.kts diff --git a/kotlin/workflow-ui/backstack-android/gradle.properties b/workflow-ui/backstack-android/gradle.properties similarity index 100% rename from kotlin/workflow-ui/backstack-android/gradle.properties rename to workflow-ui/backstack-android/gradle.properties diff --git a/kotlin/workflow-ui/backstack-android/src/main/AndroidManifest.xml b/workflow-ui/backstack-android/src/main/AndroidManifest.xml similarity index 100% rename from kotlin/workflow-ui/backstack-android/src/main/AndroidManifest.xml rename to workflow-ui/backstack-android/src/main/AndroidManifest.xml diff --git a/kotlin/workflow-ui/backstack-android/src/main/java/com/squareup/workflow/ui/backstack/BackStackConfig.kt b/workflow-ui/backstack-android/src/main/java/com/squareup/workflow/ui/backstack/BackStackConfig.kt similarity index 100% rename from kotlin/workflow-ui/backstack-android/src/main/java/com/squareup/workflow/ui/backstack/BackStackConfig.kt rename to workflow-ui/backstack-android/src/main/java/com/squareup/workflow/ui/backstack/BackStackConfig.kt diff --git a/kotlin/workflow-ui/backstack-android/src/main/java/com/squareup/workflow/ui/backstack/BackStackContainer.kt b/workflow-ui/backstack-android/src/main/java/com/squareup/workflow/ui/backstack/BackStackContainer.kt similarity index 100% rename from kotlin/workflow-ui/backstack-android/src/main/java/com/squareup/workflow/ui/backstack/BackStackContainer.kt rename to workflow-ui/backstack-android/src/main/java/com/squareup/workflow/ui/backstack/BackStackContainer.kt diff --git a/kotlin/workflow-ui/backstack-android/src/main/java/com/squareup/workflow/ui/backstack/ViewStateCache.kt b/workflow-ui/backstack-android/src/main/java/com/squareup/workflow/ui/backstack/ViewStateCache.kt similarity index 100% rename from kotlin/workflow-ui/backstack-android/src/main/java/com/squareup/workflow/ui/backstack/ViewStateCache.kt rename to workflow-ui/backstack-android/src/main/java/com/squareup/workflow/ui/backstack/ViewStateCache.kt diff --git a/kotlin/workflow-ui/backstack-android/src/main/java/com/squareup/workflow/ui/backstack/ViewStateFrame.kt b/workflow-ui/backstack-android/src/main/java/com/squareup/workflow/ui/backstack/ViewStateFrame.kt similarity index 100% rename from kotlin/workflow-ui/backstack-android/src/main/java/com/squareup/workflow/ui/backstack/ViewStateFrame.kt rename to workflow-ui/backstack-android/src/main/java/com/squareup/workflow/ui/backstack/ViewStateFrame.kt diff --git a/kotlin/workflow-ui/backstack-android/src/main/res/layout/view_stack_layout.xml b/workflow-ui/backstack-android/src/main/res/layout/view_stack_layout.xml similarity index 100% rename from kotlin/workflow-ui/backstack-android/src/main/res/layout/view_stack_layout.xml rename to workflow-ui/backstack-android/src/main/res/layout/view_stack_layout.xml diff --git a/kotlin/workflow-ui/backstack-android/src/main/res/values/ids.xml b/workflow-ui/backstack-android/src/main/res/values/ids.xml similarity index 100% rename from kotlin/workflow-ui/backstack-android/src/main/res/values/ids.xml rename to workflow-ui/backstack-android/src/main/res/values/ids.xml diff --git a/kotlin/workflow-ui/backstack-common/api/backstack-common.api b/workflow-ui/backstack-common/api/backstack-common.api similarity index 100% rename from kotlin/workflow-ui/backstack-common/api/backstack-common.api rename to workflow-ui/backstack-common/api/backstack-common.api diff --git a/kotlin/workflow-ui/backstack-common/build.gradle.kts b/workflow-ui/backstack-common/build.gradle.kts similarity index 100% rename from kotlin/workflow-ui/backstack-common/build.gradle.kts rename to workflow-ui/backstack-common/build.gradle.kts diff --git a/kotlin/workflow-ui/backstack-common/gradle.properties b/workflow-ui/backstack-common/gradle.properties similarity index 100% rename from kotlin/workflow-ui/backstack-common/gradle.properties rename to workflow-ui/backstack-common/gradle.properties diff --git a/kotlin/workflow-ui/backstack-common/src/main/java/com/squareup/workflow/ui/backstack/BackStackScreen.kt b/workflow-ui/backstack-common/src/main/java/com/squareup/workflow/ui/backstack/BackStackScreen.kt similarity index 100% rename from kotlin/workflow-ui/backstack-common/src/main/java/com/squareup/workflow/ui/backstack/BackStackScreen.kt rename to workflow-ui/backstack-common/src/main/java/com/squareup/workflow/ui/backstack/BackStackScreen.kt diff --git a/kotlin/workflow-ui/backstack-common/src/test/java/com/squareup/workflow/ui/backstack/BackStackScreenTest.kt b/workflow-ui/backstack-common/src/test/java/com/squareup/workflow/ui/backstack/BackStackScreenTest.kt similarity index 100% rename from kotlin/workflow-ui/backstack-common/src/test/java/com/squareup/workflow/ui/backstack/BackStackScreenTest.kt rename to workflow-ui/backstack-common/src/test/java/com/squareup/workflow/ui/backstack/BackStackScreenTest.kt diff --git a/kotlin/workflow-ui/core-android/api/core-android.api b/workflow-ui/core-android/api/core-android.api similarity index 100% rename from kotlin/workflow-ui/core-android/api/core-android.api rename to workflow-ui/core-android/api/core-android.api diff --git a/kotlin/workflow-ui/core-android/build.gradle.kts b/workflow-ui/core-android/build.gradle.kts similarity index 100% rename from kotlin/workflow-ui/core-android/build.gradle.kts rename to workflow-ui/core-android/build.gradle.kts diff --git a/kotlin/workflow-ui/core-android/gradle.properties b/workflow-ui/core-android/gradle.properties similarity index 100% rename from kotlin/workflow-ui/core-android/gradle.properties rename to workflow-ui/core-android/gradle.properties diff --git a/kotlin/workflow-ui/core-android/src/main/AndroidManifest.xml b/workflow-ui/core-android/src/main/AndroidManifest.xml similarity index 100% rename from kotlin/workflow-ui/core-android/src/main/AndroidManifest.xml rename to workflow-ui/core-android/src/main/AndroidManifest.xml diff --git a/kotlin/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/BackPressHandler.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/BackPressHandler.kt similarity index 100% rename from kotlin/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/BackPressHandler.kt rename to workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/BackPressHandler.kt diff --git a/kotlin/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/BindingViewRegistry.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/BindingViewRegistry.kt similarity index 100% rename from kotlin/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/BindingViewRegistry.kt rename to workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/BindingViewRegistry.kt diff --git a/kotlin/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/BuilderBinding.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/BuilderBinding.kt similarity index 100% rename from kotlin/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/BuilderBinding.kt rename to workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/BuilderBinding.kt diff --git a/kotlin/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/CompositeViewRegistry.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/CompositeViewRegistry.kt similarity index 100% rename from kotlin/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/CompositeViewRegistry.kt rename to workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/CompositeViewRegistry.kt diff --git a/kotlin/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/LayoutRunner.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/LayoutRunner.kt similarity index 100% rename from kotlin/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/LayoutRunner.kt rename to workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/LayoutRunner.kt diff --git a/kotlin/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/Lifecycles.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/Lifecycles.kt similarity index 100% rename from kotlin/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/Lifecycles.kt rename to workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/Lifecycles.kt diff --git a/kotlin/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/NamedBinding.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/NamedBinding.kt similarity index 100% rename from kotlin/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/NamedBinding.kt rename to workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/NamedBinding.kt diff --git a/kotlin/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/PickledWorkflow.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/PickledWorkflow.kt similarity index 100% rename from kotlin/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/PickledWorkflow.kt rename to workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/PickledWorkflow.kt diff --git a/kotlin/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/ViewBindingViewFactory.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/ViewBindingViewFactory.kt similarity index 100% rename from kotlin/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/ViewBindingViewFactory.kt rename to workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/ViewBindingViewFactory.kt diff --git a/kotlin/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/ViewEnvironment.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/ViewEnvironment.kt similarity index 100% rename from kotlin/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/ViewEnvironment.kt rename to workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/ViewEnvironment.kt diff --git a/kotlin/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/ViewFactory.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/ViewFactory.kt similarity index 100% rename from kotlin/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/ViewFactory.kt rename to workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/ViewFactory.kt diff --git a/kotlin/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/ViewRegistry.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/ViewRegistry.kt similarity index 100% rename from kotlin/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/ViewRegistry.kt rename to workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/ViewRegistry.kt diff --git a/kotlin/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/ViewShowRendering.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/ViewShowRendering.kt similarity index 100% rename from kotlin/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/ViewShowRendering.kt rename to workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/ViewShowRendering.kt diff --git a/kotlin/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/WorkflowFragment.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/WorkflowFragment.kt similarity index 100% rename from kotlin/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/WorkflowFragment.kt rename to workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/WorkflowFragment.kt diff --git a/kotlin/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/WorkflowLayout.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/WorkflowLayout.kt similarity index 100% rename from kotlin/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/WorkflowLayout.kt rename to workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/WorkflowLayout.kt diff --git a/kotlin/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/WorkflowRunner.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/WorkflowRunner.kt similarity index 100% rename from kotlin/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/WorkflowRunner.kt rename to workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/WorkflowRunner.kt diff --git a/kotlin/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/WorkflowRunnerViewModel.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/WorkflowRunnerViewModel.kt similarity index 100% rename from kotlin/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/WorkflowRunnerViewModel.kt rename to workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/WorkflowRunnerViewModel.kt diff --git a/kotlin/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/WorkflowViewStub.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/WorkflowViewStub.kt similarity index 100% rename from kotlin/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/WorkflowViewStub.kt rename to workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/WorkflowViewStub.kt diff --git a/kotlin/workflow-ui/core-android/src/main/res/values/ids.xml b/workflow-ui/core-android/src/main/res/values/ids.xml similarity index 100% rename from kotlin/workflow-ui/core-android/src/main/res/values/ids.xml rename to workflow-ui/core-android/src/main/res/values/ids.xml diff --git a/kotlin/workflow-ui/core-android/src/test/java/com/squareup/workflow/ui/BindingViewRegistryTest.kt b/workflow-ui/core-android/src/test/java/com/squareup/workflow/ui/BindingViewRegistryTest.kt similarity index 100% rename from kotlin/workflow-ui/core-android/src/test/java/com/squareup/workflow/ui/BindingViewRegistryTest.kt rename to workflow-ui/core-android/src/test/java/com/squareup/workflow/ui/BindingViewRegistryTest.kt diff --git a/kotlin/workflow-ui/core-android/src/test/java/com/squareup/workflow/ui/CompositeViewRegistryTest.kt b/workflow-ui/core-android/src/test/java/com/squareup/workflow/ui/CompositeViewRegistryTest.kt similarity index 100% rename from kotlin/workflow-ui/core-android/src/test/java/com/squareup/workflow/ui/CompositeViewRegistryTest.kt rename to workflow-ui/core-android/src/test/java/com/squareup/workflow/ui/CompositeViewRegistryTest.kt diff --git a/kotlin/workflow-ui/core-android/src/test/java/com/squareup/workflow/ui/TestViewFactory.kt b/workflow-ui/core-android/src/test/java/com/squareup/workflow/ui/TestViewFactory.kt similarity index 100% rename from kotlin/workflow-ui/core-android/src/test/java/com/squareup/workflow/ui/TestViewFactory.kt rename to workflow-ui/core-android/src/test/java/com/squareup/workflow/ui/TestViewFactory.kt diff --git a/kotlin/workflow-ui/core-android/src/test/java/com/squareup/workflow/ui/ViewEnvironmentTest.kt b/workflow-ui/core-android/src/test/java/com/squareup/workflow/ui/ViewEnvironmentTest.kt similarity index 100% rename from kotlin/workflow-ui/core-android/src/test/java/com/squareup/workflow/ui/ViewEnvironmentTest.kt rename to workflow-ui/core-android/src/test/java/com/squareup/workflow/ui/ViewEnvironmentTest.kt diff --git a/kotlin/workflow-ui/core-android/src/test/java/com/squareup/workflow/ui/ViewRegistryTest.kt b/workflow-ui/core-android/src/test/java/com/squareup/workflow/ui/ViewRegistryTest.kt similarity index 100% rename from kotlin/workflow-ui/core-android/src/test/java/com/squareup/workflow/ui/ViewRegistryTest.kt rename to workflow-ui/core-android/src/test/java/com/squareup/workflow/ui/ViewRegistryTest.kt diff --git a/kotlin/workflow-ui/core-android/src/test/java/com/squareup/workflow/ui/WorkflowRunnerViewModelTest.kt b/workflow-ui/core-android/src/test/java/com/squareup/workflow/ui/WorkflowRunnerViewModelTest.kt similarity index 100% rename from kotlin/workflow-ui/core-android/src/test/java/com/squareup/workflow/ui/WorkflowRunnerViewModelTest.kt rename to workflow-ui/core-android/src/test/java/com/squareup/workflow/ui/WorkflowRunnerViewModelTest.kt diff --git a/kotlin/workflow-ui/core-common/api/core-common.api b/workflow-ui/core-common/api/core-common.api similarity index 100% rename from kotlin/workflow-ui/core-common/api/core-common.api rename to workflow-ui/core-common/api/core-common.api diff --git a/kotlin/workflow-ui/core-common/build.gradle.kts b/workflow-ui/core-common/build.gradle.kts similarity index 100% rename from kotlin/workflow-ui/core-common/build.gradle.kts rename to workflow-ui/core-common/build.gradle.kts diff --git a/kotlin/workflow-ui/core-common/gradle.properties b/workflow-ui/core-common/gradle.properties similarity index 100% rename from kotlin/workflow-ui/core-common/gradle.properties rename to workflow-ui/core-common/gradle.properties diff --git a/kotlin/workflow-ui/core-common/src/main/java/com/squareup/workflow/ui/Compatible.kt b/workflow-ui/core-common/src/main/java/com/squareup/workflow/ui/Compatible.kt similarity index 100% rename from kotlin/workflow-ui/core-common/src/main/java/com/squareup/workflow/ui/Compatible.kt rename to workflow-ui/core-common/src/main/java/com/squareup/workflow/ui/Compatible.kt diff --git a/kotlin/workflow-ui/core-common/src/main/java/com/squareup/workflow/ui/Named.kt b/workflow-ui/core-common/src/main/java/com/squareup/workflow/ui/Named.kt similarity index 100% rename from kotlin/workflow-ui/core-common/src/main/java/com/squareup/workflow/ui/Named.kt rename to workflow-ui/core-common/src/main/java/com/squareup/workflow/ui/Named.kt diff --git a/kotlin/workflow-ui/core-common/src/test/java/com/squareup/workflow/ui/CompatibleTest.kt b/workflow-ui/core-common/src/test/java/com/squareup/workflow/ui/CompatibleTest.kt similarity index 100% rename from kotlin/workflow-ui/core-common/src/test/java/com/squareup/workflow/ui/CompatibleTest.kt rename to workflow-ui/core-common/src/test/java/com/squareup/workflow/ui/CompatibleTest.kt diff --git a/kotlin/workflow-ui/core-common/src/test/java/com/squareup/workflow/ui/NamedTest.kt b/workflow-ui/core-common/src/test/java/com/squareup/workflow/ui/NamedTest.kt similarity index 100% rename from kotlin/workflow-ui/core-common/src/test/java/com/squareup/workflow/ui/NamedTest.kt rename to workflow-ui/core-common/src/test/java/com/squareup/workflow/ui/NamedTest.kt diff --git a/kotlin/workflow-ui/modal-android/api/modal-android.api b/workflow-ui/modal-android/api/modal-android.api similarity index 100% rename from kotlin/workflow-ui/modal-android/api/modal-android.api rename to workflow-ui/modal-android/api/modal-android.api diff --git a/kotlin/workflow-ui/modal-android/build.gradle.kts b/workflow-ui/modal-android/build.gradle.kts similarity index 100% rename from kotlin/workflow-ui/modal-android/build.gradle.kts rename to workflow-ui/modal-android/build.gradle.kts diff --git a/kotlin/workflow-ui/modal-android/gradle.properties b/workflow-ui/modal-android/gradle.properties similarity index 100% rename from kotlin/workflow-ui/modal-android/gradle.properties rename to workflow-ui/modal-android/gradle.properties diff --git a/kotlin/workflow-ui/modal-android/src/main/AndroidManifest.xml b/workflow-ui/modal-android/src/main/AndroidManifest.xml similarity index 100% rename from kotlin/workflow-ui/modal-android/src/main/AndroidManifest.xml rename to workflow-ui/modal-android/src/main/AndroidManifest.xml diff --git a/kotlin/workflow-ui/modal-android/src/main/java/com/squareup/workflow/ui/modal/AlertContainer.kt b/workflow-ui/modal-android/src/main/java/com/squareup/workflow/ui/modal/AlertContainer.kt similarity index 100% rename from kotlin/workflow-ui/modal-android/src/main/java/com/squareup/workflow/ui/modal/AlertContainer.kt rename to workflow-ui/modal-android/src/main/java/com/squareup/workflow/ui/modal/AlertContainer.kt diff --git a/kotlin/workflow-ui/modal-android/src/main/java/com/squareup/workflow/ui/modal/ModalContainer.kt b/workflow-ui/modal-android/src/main/java/com/squareup/workflow/ui/modal/ModalContainer.kt similarity index 100% rename from kotlin/workflow-ui/modal-android/src/main/java/com/squareup/workflow/ui/modal/ModalContainer.kt rename to workflow-ui/modal-android/src/main/java/com/squareup/workflow/ui/modal/ModalContainer.kt diff --git a/kotlin/workflow-ui/modal-android/src/main/java/com/squareup/workflow/ui/modal/ModalViewContainer.kt b/workflow-ui/modal-android/src/main/java/com/squareup/workflow/ui/modal/ModalViewContainer.kt similarity index 100% rename from kotlin/workflow-ui/modal-android/src/main/java/com/squareup/workflow/ui/modal/ModalViewContainer.kt rename to workflow-ui/modal-android/src/main/java/com/squareup/workflow/ui/modal/ModalViewContainer.kt diff --git a/kotlin/workflow-ui/modal-android/src/main/res/values/ids.xml b/workflow-ui/modal-android/src/main/res/values/ids.xml similarity index 100% rename from kotlin/workflow-ui/modal-android/src/main/res/values/ids.xml rename to workflow-ui/modal-android/src/main/res/values/ids.xml diff --git a/kotlin/workflow-ui/modal-common/api/modal-common.api b/workflow-ui/modal-common/api/modal-common.api similarity index 100% rename from kotlin/workflow-ui/modal-common/api/modal-common.api rename to workflow-ui/modal-common/api/modal-common.api diff --git a/kotlin/workflow-ui/modal-common/build.gradle.kts b/workflow-ui/modal-common/build.gradle.kts similarity index 100% rename from kotlin/workflow-ui/modal-common/build.gradle.kts rename to workflow-ui/modal-common/build.gradle.kts diff --git a/kotlin/workflow-ui/modal-common/gradle.properties b/workflow-ui/modal-common/gradle.properties similarity index 100% rename from kotlin/workflow-ui/modal-common/gradle.properties rename to workflow-ui/modal-common/gradle.properties diff --git a/kotlin/workflow-ui/modal-common/src/main/java/com/squareup/workflow/ui/modal/AlertContainerScreen.kt b/workflow-ui/modal-common/src/main/java/com/squareup/workflow/ui/modal/AlertContainerScreen.kt similarity index 100% rename from kotlin/workflow-ui/modal-common/src/main/java/com/squareup/workflow/ui/modal/AlertContainerScreen.kt rename to workflow-ui/modal-common/src/main/java/com/squareup/workflow/ui/modal/AlertContainerScreen.kt diff --git a/kotlin/workflow-ui/modal-common/src/main/java/com/squareup/workflow/ui/modal/AlertScreen.kt b/workflow-ui/modal-common/src/main/java/com/squareup/workflow/ui/modal/AlertScreen.kt similarity index 100% rename from kotlin/workflow-ui/modal-common/src/main/java/com/squareup/workflow/ui/modal/AlertScreen.kt rename to workflow-ui/modal-common/src/main/java/com/squareup/workflow/ui/modal/AlertScreen.kt diff --git a/kotlin/workflow-ui/modal-common/src/main/java/com/squareup/workflow/ui/modal/HasModals.kt b/workflow-ui/modal-common/src/main/java/com/squareup/workflow/ui/modal/HasModals.kt similarity index 100% rename from kotlin/workflow-ui/modal-common/src/main/java/com/squareup/workflow/ui/modal/HasModals.kt rename to workflow-ui/modal-common/src/main/java/com/squareup/workflow/ui/modal/HasModals.kt