├── .github ├── CODEOWNERS ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── docs.yaml │ └── swift.yaml ├── .gitignore ├── .hooks └── pre-commit ├── .mise.toml ├── .swiftformat ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Documentation └── WorkflowSwiftUI Adoption Guide.md ├── LICENSE ├── MigrationGuide_v1.0.md ├── NOTICE.txt ├── Package.swift ├── README.md ├── RELEASING.md ├── Samples ├── AlertContainer │ ├── README.md │ └── Sources │ │ ├── AlertContainerScreen.swift │ │ └── AlertContainerViewController.swift ├── AsyncWorker │ ├── README.md │ └── Sources │ │ ├── AppDelegate.swift │ │ ├── AsyncWorkerWorkflow.swift │ │ ├── FakeNetwork │ │ ├── FakeNetworkManager.swift │ │ └── Model.swift │ │ └── MessageViewController.swift ├── BackStackContainer │ ├── README.md │ └── Sources │ │ ├── BackStackContainer.swift │ │ ├── BackStackScreen.swift │ │ └── ScreenWrapperViewController.swift ├── ModalContainer │ ├── README.md │ └── Sources │ │ ├── ModalContainerScreen.swift │ │ └── ModalContainerViewController.swift ├── ObservableComposition │ ├── README.md │ └── Sources │ │ ├── AppDelegate.swift │ │ ├── CounterView.swift │ │ ├── CounterWorkflow.swift │ │ ├── MultiCounterScreen.swift │ │ ├── MultiCounterView.swift │ │ └── MultiCounterWorkflow.swift ├── ObservableCounter │ ├── README.md │ └── Sources │ │ ├── AppDelegate.swift │ │ ├── CounterListModel.swift │ │ ├── CounterListScreen.swift │ │ ├── CounterListView.swift │ │ ├── CounterListWorkflow.swift │ │ └── SimpleCounterView.swift ├── Project.swift ├── SampleApp │ ├── .gitignore │ └── Sources │ │ ├── AppDelegate.swift │ │ ├── CrossFadeContainer.swift │ │ ├── DemoScreen.swift │ │ ├── DemoWorkflow.swift │ │ ├── ReversingWorkflow.swift │ │ ├── RootWorkflow.swift │ │ ├── WelcomeScreen.swift │ │ └── WelcomeWorkflow.swift ├── SnapshotTests │ └── ReferenceImages_64 │ │ └── SplitScreenContainerScreenSnapshotTests │ │ ├── test_splitRatio_custom_iPad.png │ │ ├── test_splitRatio_half_iPad.png │ │ ├── test_splitRatio_quarter_iPad.png │ │ └── test_splitRatio_third_iPad.png ├── SplitScreenContainer │ ├── DemoApp │ │ ├── AppDelegate.swift │ │ ├── BarScreen.swift │ │ ├── DemoWorkflow.swift │ │ └── FooScreen.swift │ ├── README.md │ ├── SnapshotTests │ │ └── SplitScreenContainerScreenSnapshotTests.swift │ └── Sources │ │ ├── ContainerView.swift │ │ ├── Environment+SplitScreen.swift │ │ ├── SplitScreenContainerScreen.swift │ │ └── SplitScreenContainerViewController.swift ├── TicTacToe │ ├── .gitignore │ ├── Sources │ │ ├── AppDelegate.swift │ │ ├── Authentication │ │ │ ├── AuthenticationService.swift │ │ │ ├── AuthenticationWorkflow.swift │ │ │ ├── LoadingScreen.swift │ │ │ ├── LoginScreen.swift │ │ │ ├── LoginWorkflow.swift │ │ │ └── TwoFactorScreen.swift │ │ ├── Game │ │ │ ├── Board.swift │ │ │ ├── ConfirmQuitScreen.swift │ │ │ ├── ConfirmQuitWorkflow.swift │ │ │ ├── GamePlayScreen.swift │ │ │ ├── GameState.swift │ │ │ ├── NewGameScreen.swift │ │ │ ├── RunGameWorkflow.swift │ │ │ └── TakeTurnsWorkflow.swift │ │ └── Main │ │ │ └── MainWorkflow.swift │ └── Tests │ │ ├── AuthenticationWorkflowTests.swift │ │ ├── ConfirmQuitWorkflowTests.swift │ │ ├── LoginWorkflowTests.swift │ │ ├── MainWorkflowTests.swift │ │ ├── RunGameWorkflowTests.swift │ │ └── TakeTurnsWorkflowTests.swift ├── Tuist │ ├── Config.swift │ ├── Package.resolved │ ├── Package.swift │ └── ProjectDescriptionHelpers │ │ └── Project+Workflow.swift ├── Tutorial │ ├── .gitignore │ ├── AppHost │ │ └── Sources │ │ │ └── AppDelegate.swift │ ├── Frameworks │ │ ├── Tutorial1Complete │ │ │ └── Sources │ │ │ │ ├── Todo │ │ │ │ ├── Edit │ │ │ │ │ └── TodoEditSampleViewController.swift │ │ │ │ ├── List │ │ │ │ │ └── TodoListSampleViewController.swift │ │ │ │ └── Model │ │ │ │ │ └── TodoModel.swift │ │ │ │ ├── TutorialHostingViewController.swift │ │ │ │ └── Welcome │ │ │ │ ├── WelcomeScreen.swift │ │ │ │ └── WelcomeWorkflow.swift │ │ ├── Tutorial2Complete │ │ │ └── Sources │ │ │ │ ├── RootWorkflow.swift │ │ │ │ ├── Todo │ │ │ │ ├── Edit │ │ │ │ │ └── TodoEditSampleViewController.swift │ │ │ │ ├── List │ │ │ │ │ ├── TodoListScreen.swift │ │ │ │ │ └── TodoListWorkflow.swift │ │ │ │ └── Model │ │ │ │ │ └── TodoModel.swift │ │ │ │ ├── TutorialHostingViewController.swift │ │ │ │ └── Welcome │ │ │ │ ├── WelcomeScreen.swift │ │ │ │ └── WelcomeWorkflow.swift │ │ ├── Tutorial3Complete │ │ │ └── Sources │ │ │ │ ├── RootWorkflow.swift │ │ │ │ ├── Todo │ │ │ │ ├── Edit │ │ │ │ │ ├── TodoEditScreen.swift │ │ │ │ │ └── TodoEditWorkflow.swift │ │ │ │ ├── List │ │ │ │ │ ├── TodoListScreen.swift │ │ │ │ │ └── TodoListWorkflow.swift │ │ │ │ └── Model │ │ │ │ │ └── TodoModel.swift │ │ │ │ ├── TutorialHostingViewController.swift │ │ │ │ └── Welcome │ │ │ │ ├── WelcomeScreen.swift │ │ │ │ └── WelcomeWorkflow.swift │ │ ├── Tutorial4Complete │ │ │ ├── Sources │ │ │ │ ├── RootWorkflow.swift │ │ │ │ ├── Todo │ │ │ │ │ ├── Edit │ │ │ │ │ │ ├── TodoEditScreen.swift │ │ │ │ │ │ └── TodoEditWorkflow.swift │ │ │ │ │ ├── List │ │ │ │ │ │ ├── TodoListScreen.swift │ │ │ │ │ │ └── TodoListWorkflow.swift │ │ │ │ │ ├── Model │ │ │ │ │ │ └── TodoModel.swift │ │ │ │ │ └── TodoWorkflow.swift │ │ │ │ ├── TutorialHostingViewController.swift │ │ │ │ └── Welcome │ │ │ │ │ ├── WelcomeScreen.swift │ │ │ │ │ └── WelcomeWorkflow.swift │ │ │ └── Tests │ │ │ │ └── TutorialTests.swift │ │ ├── Tutorial5Complete │ │ │ ├── Sources │ │ │ │ ├── RootWorkflow.swift │ │ │ │ ├── Todo │ │ │ │ │ ├── Edit │ │ │ │ │ │ ├── TodoEditScreen.swift │ │ │ │ │ │ └── TodoEditWorkflow.swift │ │ │ │ │ ├── List │ │ │ │ │ │ ├── TodoListScreen.swift │ │ │ │ │ │ └── TodoListWorkflow.swift │ │ │ │ │ ├── Model │ │ │ │ │ │ └── TodoModel.swift │ │ │ │ │ └── TodoWorkflow.swift │ │ │ │ ├── TutorialHostingViewController.swift │ │ │ │ └── Welcome │ │ │ │ │ ├── WelcomeScreen.swift │ │ │ │ │ └── WelcomeWorkflow.swift │ │ │ └── Tests │ │ │ │ ├── RootWorkflowTests.swift │ │ │ │ ├── TodoEditWorkflowTests.swift │ │ │ │ ├── TodoListWorkflowTests.swift │ │ │ │ ├── TodoWorkflowTests.swift │ │ │ │ ├── TutorialTests.swift │ │ │ │ └── WelcomeWorkflowTests.swift │ │ ├── TutorialBase │ │ │ ├── Sources │ │ │ │ ├── Todo │ │ │ │ │ ├── Edit │ │ │ │ │ │ └── TodoEditSampleViewController.swift │ │ │ │ │ ├── List │ │ │ │ │ │ └── TodoListSampleViewController.swift │ │ │ │ │ └── Model │ │ │ │ │ │ └── TodoModel.swift │ │ │ │ ├── TutorialHostingViewController.swift │ │ │ │ └── Welcome │ │ │ │ │ └── WelcomeSampleViewController.swift │ │ │ └── Tests │ │ │ │ └── TutorialTests.swift │ │ └── TutorialViews │ │ │ └── Sources │ │ │ ├── TodoEditView.swift │ │ │ ├── TodoListView.swift │ │ │ └── WelcomeView.swift │ ├── Project.swift │ ├── README.md │ ├── Tutorial1.md │ ├── Tutorial2.md │ ├── Tutorial3.md │ ├── Tutorial4.md │ ├── Tutorial5.md │ └── images │ │ ├── empty-todolist.png │ │ ├── full-edit-flow.gif │ │ ├── missing-map-output.png │ │ ├── new-screen-todolist.png │ │ ├── new-screen.png │ │ ├── new-todolist-workflow.png │ │ ├── new-workflow.png │ │ ├── tut2-todolist-example.png │ │ ├── welcome-to-todolist.gif │ │ ├── welcome.png │ │ ├── workflow-file-location.png │ │ └── workflow-name.png ├── WorkflowCombineSampleApp │ ├── WorkflowCombineSampleApp │ │ ├── AppDelegate.swift │ │ ├── DemoViewController.swift │ │ ├── DemoWorker.swift │ │ └── DemoWorkflow.swift │ └── WorkflowCombineSampleAppUnitTests │ │ └── DemoWorkflowTests.swift └── Workspace.swift ├── Scripts ├── generate_docs.sh └── git-format-staged ├── TestingSupport └── AppHost │ └── Sources │ └── AppDelegate.swift ├── Tooling └── Templates │ ├── Screen (View Controller).xctemplate │ ├── TemplateIcon.png │ ├── TemplateIcon@2x.png │ ├── TemplateInfo.plist │ └── ___FILEBASENAME___Screen.swift │ ├── Workflow (Verbose).xctemplate │ ├── Default │ │ └── ___FILEBASENAME___Workflow.swift │ ├── TemplateIcon.png │ ├── TemplateIcon@2x.png │ ├── TemplateInfo.plist │ ├── generateWorkerReactiveSwift │ │ ├── ___FILEBASENAME___Worker.swift │ │ └── ___FILEBASENAME___Workflow.swift │ └── generateWorkerRxSwift │ │ ├── ___FILEBASENAME___Worker.swift │ │ └── ___FILEBASENAME___Workflow.swift │ └── install-xcode-templates.sh ├── ViewEnvironment └── Sources │ ├── EnvironmentValues+ViewEnvironment.swift │ ├── ViewEnvironment.swift │ └── ViewEnvironmentKey.swift ├── ViewEnvironmentUI ├── README.md ├── Sources │ ├── UIView+ViewEnvironmentPropagating.swift │ ├── UIViewController+ViewEnvironmentPropagating.swift │ ├── ViewEnvironmentObserving.swift │ ├── ViewEnvironmentPropagating.swift │ └── ViewEnvironmentPropagationNode.swift └── Tests │ ├── ViewEnvironment+Test.swift │ └── ViewEnvironmentObservingTests.swift ├── Workflow ├── Sources │ ├── AnyWorkflow.swift │ ├── AnyWorkflowConvertible.swift │ ├── ApplyContext.swift │ ├── Debugging.swift │ ├── DispatchQueue+Workflow.swift │ ├── Lifetime.swift │ ├── RenderContext.swift │ ├── Sink.swift │ ├── StateMutationSink.swift │ ├── SubtreeManager.swift │ ├── Workflow.swift │ ├── WorkflowAction.swift │ ├── WorkflowHost.swift │ ├── WorkflowLogger.swift │ ├── WorkflowNode.swift │ └── WorkflowObserver.swift └── Tests │ ├── AnyWorkflowActionTests.swift │ ├── AnyWorkflowTests.swift │ ├── ApplyContextTests.swift │ ├── ConcurrencyTests.swift │ ├── DebuggingTests.swift │ ├── HostContextTests.swift │ ├── PerformanceTests.swift │ ├── StateMutationSinkTests.swift │ ├── SubtreeManagerTests.swift │ ├── TestUtilities.swift │ ├── WorkflowHostTests.swift │ ├── WorkflowNodeTests.swift │ └── WorkflowObserverTests.swift ├── WorkflowCombine ├── Sources │ ├── Logger.swift │ ├── Publisher+Extensions.swift │ ├── PublisherWorkflow.swift │ └── Worker.swift ├── Testing │ ├── PublisherTesting.swift │ └── WorkerTesting.swift ├── TestingTests │ ├── PublisherTests.swift │ └── TestingTests.swift └── Tests │ ├── PublisherTests.swift │ └── WorkerTests.swift ├── WorkflowConcurrency ├── Sources │ ├── AsyncOperationWorker.swift │ ├── Logger.swift │ └── Worker.swift ├── Testing │ └── WorkerTesting.swift ├── TestingTests │ └── TestingTests.swift └── Tests │ ├── AsyncOperationWorkerTests.swift │ └── WorkerTests.swift ├── WorkflowReactiveSwift ├── Sources │ ├── Logger.swift │ ├── QueueScheduler+Workflow.swift │ ├── SignalProducerWorkflow.swift │ ├── SignalWorker.swift │ └── Worker.swift ├── Testing │ ├── SignalProducerWorkflowTesting.swift │ └── WorkerTesting.swift ├── TestingTests │ ├── SignalProducerTests.swift │ └── TestingTests.swift └── Tests │ ├── SignalProducerTests.swift │ ├── SignalTests.swift │ └── WorkerTests.swift ├── WorkflowRxSwift ├── Sources │ ├── Logger.swift │ ├── ObservableWorkflow.swift │ └── Worker.swift ├── Testing │ ├── ObservableTesting.swift │ └── WorkerTesting.swift ├── TestingTests │ ├── ObservableTests.swift │ └── TestingTests.swift └── Tests │ ├── ObservableTests.swift │ ├── Rx+ReactiveWorkers.swift │ └── WorkerTests.swift ├── WorkflowSwiftUI ├── Sources │ ├── ActionModel.swift │ ├── Bindable+Store.swift │ ├── Derived │ │ ├── AreOrderedSetsDuplicates.swift │ │ ├── ObservableState.swift │ │ └── ObservationStateRegistrar.swift │ ├── Exports.swift │ ├── Macros.swift │ ├── ObservableModel.swift │ ├── ObservableScreen+Preview.swift │ ├── ObservableScreen.swift │ ├── RenderContext+ObservableModel.swift │ ├── StateAccessor.swift │ ├── Store+Preview.swift │ ├── Store.swift │ ├── UIViewController+Orientation.swift │ └── Workflow+Preview.swift └── Tests │ ├── Derived │ └── ObservableStateTests.swift │ ├── NestedStoreTests.swift │ ├── ObservableScreenTests.swift │ ├── PreferredContentSizeTests.swift │ ├── StoreTests.swift │ └── XCTestCase+AppHost.swift ├── WorkflowSwiftUIMacros ├── Sources │ ├── Derived │ │ ├── Availability.swift │ │ ├── Extensions.swift │ │ └── ObservableStateMacro.swift │ └── Plugins.swift └── Tests │ └── Derived │ └── ObservableStateMacroTests.swift ├── WorkflowTesting ├── Sources │ ├── Internal │ │ ├── AppliedAction.swift │ │ ├── RenderExpectations.swift │ │ └── RenderTester+TestContext.swift │ ├── RenderTesterResult.swift │ ├── WorkflowActionTester.swift │ └── WorkflowRenderTester.swift └── Tests │ ├── TestingFrameworkCompatibilityTests.swift │ ├── WorkflowActionTesterTests.swift │ ├── WorkflowRenderTesterFailureTests.swift │ └── WorkflowRenderTesterTests.swift └── WorkflowUI ├── Sources ├── Hosting │ └── WorkflowHostingController.swift ├── ModuleExports.swift ├── Observation │ ├── WorkflowUIEvents.swift │ ├── WorkflowUIObserver.swift │ └── WorkflowUIViewController.swift ├── Screen │ ├── AdaptedEnvironmentScreen.swift │ ├── AnyScreen │ │ └── AnyScreen.swift │ ├── Screen.swift │ ├── ScreenContaining.swift │ └── ScreenViewController.swift └── ViewControllerDescription │ ├── DescribedViewController.swift │ ├── UIViewController+Extensions.swift │ └── ViewControllerDescription.swift └── Tests ├── AdaptedEnvironmentScreenTests.swift ├── DescribedViewControllerTests.swift ├── ScreenContainingTests.swift ├── UIViewControllerExtensionTests.swift ├── ViewControllerDescriptionTests.swift ├── WorkflowHostingControllerTests.swift ├── WorkflowUIObservationTestCase.swift ├── WorkflowUIViewControllerTests.swift └── XCTestCase+Extensions.swift /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # This file configures code owners (https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners), to automatically add reviewers to PRs. 2 | 3 | * @square/foundation-ios 4 | 5 | # WorkflowUI, WorkflowSwiftUI 6 | /Workflow*UI*/ @square/foundation-ios @square/ui-systems-ios 7 | 8 | # ViewEnvironment, ViewEnvironmentUI 9 | /ViewEnvironment*/ @square/foundation-ios @square/ui-systems-ios 10 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | ## Checklist 3 | 4 | - [ ] Unit Tests 5 | - [ ] UI Tests 6 | - [ ] Snapshot Tests (iOS only) 7 | - [ ] I have made corresponding changes to the documentation 8 | -------------------------------------------------------------------------------- /.github/workflows/docs.yaml: -------------------------------------------------------------------------------- 1 | name: Generate and publish docs 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | 7 | env: 8 | # Xcode 16.3 gets us Swift 6.1, required for docc merge 9 | XCODE_VERSION: 16.3 10 | 11 | jobs: 12 | build: 13 | name: Generate API docs and publish to GitHub pages 14 | # macos-15 is required for Xcode 16.3 15 | runs-on: macos-15 16 | 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | - uses: jdx/mise-action@5083fe46898c414b2475087cc79da59e7da859e8 21 | - uses: maxim-lobanov/setup-xcode@60606e260d2fc5762a71e64e74b2174e8ea3c8bd 22 | with: 23 | xcode-version: ${{ env.XCODE_VERSION }} 24 | 25 | - name: Install dependencies 26 | run: tuist install --path Samples 27 | 28 | - name: Generate project 29 | run: tuist generate --path Samples --no-open 30 | 31 | - name: Generate Docs 32 | run: Scripts/generate_docs.sh 33 | 34 | - name: Deploy to GitHub Pages 35 | uses: crazy-max/ghaction-github-pages@df5cc2bfa78282ded844b354faee141f06b41865 36 | with: 37 | target_branch: gh-pages 38 | build_dir: generated_docs 39 | env: 40 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # macOS 2 | .DS_Store 3 | 4 | # Ruby and CocoaPods cruft 5 | .bundle/ 6 | gen/ 7 | Pods/ 8 | 9 | # Swift Package Manager 10 | .build/ 11 | .swiftpm/ 12 | 13 | # Xcode 14 | xcuserdata/ 15 | 16 | # Tuist 17 | /Derived 18 | /Samples/Derived 19 | /Samples/Tutorial/Derived 20 | /Samples/*.xcodeproj 21 | /Samples/*.xcworkspace 22 | /*.xcodeproj 23 | 24 | # ios-snapshot-test-case Failure Diffs 25 | FailureDiffs/ 26 | -------------------------------------------------------------------------------- /.hooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Do not run on merge 4 | if [[ $(git rev-parse -q --verify MERGE_HEAD) ]]; then 5 | exit 0 6 | fi 7 | 8 | ROOT="$(git rev-parse --show-toplevel)" 9 | SWIFT_FORMAT="mise x -- swiftformat" 10 | GIT_FORMAT_STAGED="$ROOT/Scripts/git-format-staged" 11 | CONFIG="${ROOT}/.swiftformat" 12 | 13 | $SWIFT_FORMAT --version 1>/dev/null 2>&1 14 | if [ $? -eq 0 ] 15 | then 16 | $GIT_FORMAT_STAGED --formatter "$SWIFT_FORMAT stdin --config "$CONFIG" --stdinpath '{}'" "*.swift" 17 | fi 18 | -------------------------------------------------------------------------------- /.mise.toml: -------------------------------------------------------------------------------- 1 | [tools] 2 | tuist = "4.23.0" 3 | swiftformat = "0.54.2" 4 | 5 | [settings] 6 | # do not try to read versions from .nvmrc, .ruby-version, etc. 7 | legacy_version_file = false 8 | -------------------------------------------------------------------------------- /.swiftformat: -------------------------------------------------------------------------------- 1 | # file config 2 | 3 | --swiftversion 5.9 4 | --exclude Pods,Tooling,**Dummy.swift 5 | 6 | # format config 7 | 8 | --indent 4 9 | 10 | --wraparguments before-first 11 | --importgrouping testable-bottom 12 | 13 | --enable blankLinesBetweenScopes 14 | --enable consecutiveSpaces 15 | --enable duplicateImports 16 | --enable elseOnSameLine 17 | --enable linebreakAtEndOfFile 18 | --enable redundantParens # https://google.github.io/swift/#parentheses, https://google.github.io/swift/#enum-cases, https://google.github.io/swift/#trailing-closures 19 | --enable semicolons 20 | --enable sortedImports 21 | --enable spaceAroundBraces 22 | --enable spaceAroundBrackets 23 | --enable spaceAroundOperators 24 | --enable spaceInsideBraces 25 | --enable specifiers 26 | --enable trailingSpace # https://google.github.io/swift/#horizontal-whitespace 27 | --enable wrapMultilineStatementBraces 28 | 29 | --allman false 30 | --binarygrouping none 31 | --closingparen balanced 32 | --commas always 33 | --conflictmarkers reject 34 | --decimalgrouping none 35 | --elseposition same-line 36 | --empty void 37 | --exponentcase lowercase 38 | --exponentgrouping disabled 39 | --extensionacl on-declarations 40 | --fractiongrouping disabled 41 | --fragment false 42 | --hexgrouping none 43 | --hexliteralcase uppercase 44 | --ifdef no-indent 45 | --indentcase false 46 | --linebreaks lf 47 | --maxwidth none 48 | --nospaceoperators 49 | --nowrapoperators 50 | --octalgrouping none 51 | --operatorfunc spaced 52 | --patternlet inline 53 | --self init-only 54 | --selfrequired 55 | --semicolons inline 56 | --specifierorder 57 | --tabwidth unspecified 58 | --trailingclosures 59 | --trimwhitespace always 60 | --wrapcollections before-first 61 | --wrapparameters preserve 62 | --xcodeindentation disabled 63 | 64 | --disable unusedArguments 65 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | If you would like to contribute code to Workflow you can do so through GitHub by 5 | forking the repository and sending a pull request. 6 | 7 | When submitting code, please make every effort to follow existing conventions 8 | and style in order to keep the code as readable as possible. Please also make 9 | sure your code compiles by running `./gradlew clean build`. If you're using IntelliJ IDEA, 10 | we use [Square's code style definitions][2]. 11 | 12 | Before your code can be accepted into the project you must also sign the 13 | [Individual Contributor License Agreement (CLA)][1]. 14 | 15 | [1]: https://spreadsheets.google.com/spreadsheet/viewform?formkey=dDViT2xzUHAwRkI3X3k5Z0lQM091OGc6MQ&ndplr=1 16 | [2]: https://github.com/square/java-code-styles 17 | -------------------------------------------------------------------------------- /NOTICE.txt: -------------------------------------------------------------------------------- 1 | Part of the distributed code is also derived in part from 2 | https://github.com/pointfreeco/swift-composable-architecture, licensed under MIT 3 | (https://github.com/pointfreeco/swift-composable-architecture/blob/main/LICENSE). 4 | Copyright (c) 2020 Point-Free, Inc. 5 | 6 | Part of the distributed code is also derived in part from 7 | https://github.com/apple/swift licensed under Apache 8 | (https://github.com/apple/swift/blob/main/LICENSE.txt). Copyright 2024 Apple, 9 | Inc. 10 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | # Releasing workflow 2 | 3 | ## Production Releases 4 | 5 | Workflow is now vended exclusively via Swift Package Manager. 6 | 7 | Create the release on GitHub: 8 | 1. Go to the [Releases](https://github.com/square/workflow-swift/releases) and `Draft a new release`. 9 | 1. `Choose a tag` and create a tag for the new version. ex: `v1.0.0`. 10 | 1. `Generate release notes`. 11 | 1. Ensure the `Title` corresponds to the version we're publishing and the generated `Release Notes` are accurate. 12 | 1. Hit `Publish release`. 13 | 14 | [Square specific] To make the new version available internally, [bump the version](https://go/spm-bump-dependency) through SPM. 15 | -------------------------------------------------------------------------------- /Samples/AlertContainer/README.md: -------------------------------------------------------------------------------- 1 | # AlertContainer 2 | 3 | Container to display alert screens 4 | 5 | -------------------------------------------------------------------------------- /Samples/AlertContainer/Sources/AlertContainerScreen.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import WorkflowUI 18 | 19 | /// An `AlertContainerScreen` displays a base screen with an optional alert over top of it. 20 | public struct AlertContainerScreen: Screen { 21 | /// The base screen to show underneath any visible alert. 22 | public var baseScreen: BaseScreen 23 | 24 | /// The presented alert. 25 | public var alert: Alert? 26 | 27 | public init(baseScreen: BaseScreen, alert: Alert? = nil) { 28 | self.baseScreen = baseScreen 29 | self.alert = alert 30 | } 31 | 32 | public func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { 33 | AlertContainerViewController.description(for: self, environment: environment) 34 | } 35 | } 36 | 37 | public struct Alert { 38 | public var title: String 39 | public var message: String 40 | public var actions: [AlertAction] 41 | 42 | public init(title: String, message: String, actions: [AlertAction]) { 43 | self.title = title 44 | self.message = message 45 | self.actions = actions 46 | } 47 | } 48 | 49 | public struct AlertAction { 50 | public var title: String 51 | public var style: Style 52 | public var handler: () -> Void 53 | 54 | public init(title: String, style: Style, handler: @escaping () -> Void) { 55 | self.title = title 56 | self.style = style 57 | self.handler = handler 58 | } 59 | } 60 | 61 | extension AlertAction { 62 | public enum Style { 63 | case `default` 64 | case cancel 65 | case destructive 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Samples/AsyncWorker/README.md: -------------------------------------------------------------------------------- 1 | # AsyncWorker 2 | 3 | Demonstrates how to create a `WorkflowConcurrency` async `Worker`. 4 | 5 | This is an example of how you would create a closure based network request from within the async function of a `WorkflowConcurrency` `Worker`. 6 | 7 | `AsyncWorkerWorkflow.swift` contains the `Worker` implementation. 8 | -------------------------------------------------------------------------------- /Samples/AsyncWorker/Sources/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // AsyncWorker 4 | // 5 | // Created by Mark Johnson on 6/16/22. 6 | // 7 | 8 | import UIKit 9 | import WorkflowUI 10 | 11 | @main 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | var window: UIWindow? 14 | 15 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 16 | let window = UIWindow(frame: UIScreen.main.bounds) 17 | 18 | window.rootViewController = WorkflowHostingController(workflow: AsyncWorkerWorkflow()) 19 | self.window = window 20 | window.makeKeyAndVisible() 21 | return true 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Samples/AsyncWorker/Sources/FakeNetwork/FakeNetworkManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FakeNetworkManager.swift 3 | // AsyncWorker 4 | // 5 | // Created by Mark Johnson on 6/16/22. 6 | // 7 | 8 | import Foundation 9 | 10 | class FakeNetworkManager { 11 | static func makeFakeNetworkRequest() -> FakeRequest { 12 | FakeRequest() 13 | } 14 | } 15 | 16 | class FakeRequest { 17 | enum FakeRequestError: Error { 18 | case cancelled 19 | } 20 | 21 | var cancelled: Bool = false 22 | 23 | func cancel() { 24 | cancelled = true 25 | } 26 | 27 | func perform(completion: @escaping (Result) -> Void) { 28 | DispatchQueue.global().asyncAfter(deadline: .now() + .seconds(1)) { 29 | guard !self.cancelled else { 30 | completion(.failure(FakeRequestError.cancelled)) 31 | return 32 | } 33 | 34 | completion(.success(Model(message: "Request Successful!"))) 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Samples/AsyncWorker/Sources/FakeNetwork/Model.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Model.swift 3 | // AsyncWorker 4 | // 5 | // Created by Mark Johnson on 6/16/22. 6 | // 7 | 8 | import Foundation 9 | 10 | struct Model { 11 | let message: String 12 | } 13 | -------------------------------------------------------------------------------- /Samples/AsyncWorker/Sources/MessageViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MessageViewController.swift 3 | // AsyncWorker 4 | // 5 | // Created by Mark Johnson on 6/21/22. 6 | // 7 | 8 | import UIKit 9 | import Workflow 10 | import WorkflowUI 11 | 12 | struct MessageScreen: Screen { 13 | let model: Model 14 | 15 | func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { 16 | MessageViewController.description(for: self, environment: environment) 17 | } 18 | } 19 | 20 | class MessageViewController: ScreenViewController { 21 | let label = UILabel() 22 | 23 | override func loadView() { 24 | label.text = screen.model.message 25 | view = label 26 | } 27 | 28 | override func screenDidChange(from previousScreen: MessageScreen, previousEnvironment: ViewEnvironment) { 29 | label.text = screen.model.message 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Samples/BackStackContainer/README.md: -------------------------------------------------------------------------------- 1 | # BackStackContainer 2 | 3 | An example of how a back stack container could be implemented allowing for declarative navigation backed by a UINavigationController. 4 | 5 | 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. 6 | -------------------------------------------------------------------------------- /Samples/ModalContainer/README.md: -------------------------------------------------------------------------------- 1 | # ModalContainer 2 | 3 | Container to display screens modally 4 | -------------------------------------------------------------------------------- /Samples/ModalContainer/Sources/ModalContainerScreen.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import WorkflowUI 18 | 19 | /// A `ModalContainerScreen` displays a base screen and optionally one or more modals on top of it. 20 | public struct ModalContainerScreen: Screen { 21 | /// The base screen to show underneath any modally presented screens. 22 | public let baseScreen: BaseScreen 23 | 24 | /// Modally presented screens 25 | public let modals: [ModalContainerScreenModal] 26 | 27 | public init(baseScreen: BaseScreen, modals: [ModalContainerScreenModal]) { 28 | self.baseScreen = baseScreen 29 | self.modals = modals 30 | } 31 | 32 | public func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { 33 | ModalContainerViewController.description(for: self, environment: environment) 34 | } 35 | } 36 | 37 | /// Represents a single screen to be displayed modally 38 | public struct ModalContainerScreenModal { 39 | public enum Style: Equatable { 40 | // full screen modal presentation 41 | case fullScreen 42 | // formsheet or pagesheet like modal presentation 43 | case sheet 44 | } 45 | 46 | /// The screen to be displayed 47 | public var screen: AnyScreen 48 | 49 | /// A bool used to specify whether presentation should be animated 50 | public var animated: Bool 51 | 52 | /// The style in which the screen should be presented 53 | public var style: Style 54 | 55 | /// A key used to differentiate modal screens during updates 56 | public var key: AnyHashable 57 | 58 | public init(screen: AnyScreen, style: Style = .fullScreen, key: some Hashable, animated: Bool = true) { 59 | self.screen = screen 60 | self.style = style 61 | self.key = AnyHashable(key) 62 | self.animated = animated 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Samples/ObservableComposition/README.md: -------------------------------------------------------------------------------- 1 | # ObservableComposition 2 | 3 | This demo shows how to compose views using child workflows. `CounterWorkflow` renders a model to be used in `CounterView`. `MultiCounterWorkflow` renders multiple child `CounterWorkflow`s, and `MultiCounterView` composes multiple child `CounterView`s. 4 | -------------------------------------------------------------------------------- /Samples/ObservableComposition/Sources/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Workflow 3 | import WorkflowUI 4 | 5 | @main 6 | class AppDelegate: UIResponder, UIApplicationDelegate { 7 | var window: UIWindow? 8 | 9 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 10 | let root = WorkflowHostingController( 11 | workflow: MultiCounterWorkflow().mapRendering(MultiCounterScreen.init) 12 | ) 13 | root.view.backgroundColor = .systemBackground 14 | 15 | window = UIWindow(frame: UIScreen.main.bounds) 16 | window?.rootViewController = root 17 | window?.makeKeyAndVisible() 18 | 19 | return true 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Samples/ObservableComposition/Sources/CounterView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import ViewEnvironment 3 | import WorkflowSwiftUI 4 | 5 | struct CounterView: View { 6 | typealias Model = CounterModel 7 | 8 | let store: Store 9 | let key: String 10 | 11 | var body: some View { 12 | let _ = Self._printChanges() 13 | WithPerceptionTracking { 14 | let _ = print("Evaluated CounterView[\(key)] body") 15 | HStack { 16 | Text(store.info.name) 17 | 18 | Spacer() 19 | 20 | Button { 21 | store.send(.decrement) 22 | } label: { 23 | Image(systemName: "minus") 24 | } 25 | 26 | Text("\(store.count)") 27 | .monospacedDigit() 28 | 29 | Button { 30 | store.send(.increment) 31 | } label: { 32 | Image(systemName: "plus") 33 | } 34 | 35 | if let maxValue = store.maxValue { 36 | Text("(max \(maxValue))") 37 | } 38 | } 39 | } 40 | } 41 | } 42 | 43 | #if DEBUG 44 | 45 | #Preview { 46 | CounterView( 47 | store: .preview( 48 | state: .init( 49 | count: 0, 50 | info: .init( 51 | name: "Preview counter", 52 | stepSize: 1 53 | ) 54 | ) 55 | ), 56 | key: "preview" 57 | ) 58 | .padding() 59 | } 60 | 61 | #endif 62 | -------------------------------------------------------------------------------- /Samples/ObservableComposition/Sources/MultiCounterScreen.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Perception 3 | import SwiftUI 4 | import ViewEnvironment 5 | import Workflow 6 | import WorkflowSwiftUI 7 | 8 | struct MultiCounterScreen: ObservableScreen { 9 | let model: MultiCounterModel 10 | 11 | static func makeView(store: Store) -> some View { 12 | MultiCounterView(store: store) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Samples/ObservableCounter/README.md: -------------------------------------------------------------------------------- 1 | # ObservableCounter 2 | 3 | This demo shows a single workflow with observable state, which contains an array of nested observable states. In `CounterListView`, the nested states are scoped into bindable stores. The `SimpleCounterView` takes a vanilla binding to an `Int` and has no knowledge of WorkflowSwiftUI. 4 | -------------------------------------------------------------------------------- /Samples/ObservableCounter/Sources/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Workflow 3 | import WorkflowUI 4 | 5 | @main 6 | class AppDelegate: UIResponder, UIApplicationDelegate { 7 | var window: UIWindow? 8 | 9 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 10 | let root = WorkflowHostingController( 11 | workflow: CounterListWorkflow().mapRendering(CounterListScreen.init) 12 | ) 13 | root.view.backgroundColor = .systemBackground 14 | 15 | window = UIWindow(frame: UIScreen.main.bounds) 16 | window?.rootViewController = root 17 | window?.makeKeyAndVisible() 18 | 19 | return true 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Samples/ObservableCounter/Sources/CounterListModel.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import WorkflowSwiftUI 3 | 4 | typealias CounterListModel = StateAccessor 5 | 6 | @ObservableState 7 | struct CounterListState { 8 | var counters: [Counter] 9 | 10 | @ObservableState 11 | struct Counter: Identifiable { 12 | let id = UUID() 13 | var count: Int 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Samples/ObservableCounter/Sources/CounterListScreen.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import WorkflowSwiftUI 3 | 4 | struct CounterListScreen: ObservableScreen { 5 | let model: CounterListModel 6 | 7 | static func makeView(store: Store) -> some View { 8 | CounterListView(store: store) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Samples/ObservableCounter/Sources/CounterListView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import WorkflowSwiftUI 3 | 4 | struct CounterListView: View { 5 | @Perception.Bindable 6 | var store: Store 7 | 8 | var body: some View { 9 | // These print statements show the effect of state changes on body evaluations. 10 | let _ = print("CounterListView.body") 11 | WithPerceptionTracking { 12 | VStack { 13 | ForEach(store.scope(collection: \.counters)) { counter in 14 | @Perception.Bindable var counter = counter 15 | 16 | WithPerceptionTracking { 17 | let _ = print("CounterListView.body.ForEach.body") 18 | SimpleCounterView(count: $counter.count) 19 | } 20 | } 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Samples/ObservableCounter/Sources/CounterListWorkflow.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftUI 3 | import Workflow 4 | import WorkflowSwiftUI 5 | 6 | struct CounterListWorkflow: Workflow { 7 | typealias State = CounterListState 8 | typealias Model = CounterListModel 9 | typealias Rendering = Model 10 | 11 | func makeInitialState() -> State { 12 | State(counters: [.init(count: 0), .init(count: 0), .init(count: 0)]) 13 | } 14 | 15 | func render( 16 | state: State, 17 | context: RenderContext 18 | ) -> Rendering { 19 | print("State: \(state.counters.map(\.count))") 20 | return context.makeStateAccessor(state: state) 21 | } 22 | } 23 | 24 | #Preview { 25 | CounterListWorkflow() 26 | .mapRendering(CounterListScreen.init) 27 | .workflowPreview() 28 | } 29 | -------------------------------------------------------------------------------- /Samples/ObservableCounter/Sources/SimpleCounterView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct SimpleCounterView: View { 4 | @Binding 5 | var count: Int 6 | 7 | var body: some View { 8 | let _ = print("SimpleCounterView.body") 9 | HStack { 10 | Button { 11 | count -= 1 12 | } label: { 13 | Image(systemName: "minus") 14 | } 15 | 16 | Text("\(count)") 17 | .monospacedDigit() 18 | 19 | Button { 20 | count += 1 21 | } label: { 22 | Image(systemName: "plus") 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Samples/SampleApp/.gitignore: -------------------------------------------------------------------------------- 1 | Podfile.lock 2 | -------------------------------------------------------------------------------- /Samples/SampleApp/Sources/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import UIKit 18 | import WorkflowUI 19 | 20 | @main 21 | class AppDelegate: UIResponder, UIApplicationDelegate { 22 | var window: UIWindow? 23 | 24 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 25 | window = UIWindow(frame: UIScreen.main.bounds) 26 | 27 | window?.rootViewController = WorkflowHostingController(workflow: RootWorkflow()) 28 | 29 | window?.makeKeyAndVisible() 30 | 31 | return true 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Samples/SampleApp/Sources/ReversingWorkflow.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import Workflow 18 | import WorkflowUI 19 | 20 | // MARK: Input and Output 21 | 22 | /// This is a stateless workflow. It only used the properties sent from its parent to render a result. 23 | struct ReversingWorkflow: Workflow { 24 | typealias Rendering = String 25 | typealias Output = Never 26 | typealias State = Void 27 | 28 | var text: String 29 | } 30 | 31 | // MARK: Rendering 32 | 33 | extension ReversingWorkflow { 34 | func render(state: ReversingWorkflow.State, context: RenderContext) -> String { 35 | String(text.reversed()) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Samples/SampleApp/Sources/RootWorkflow.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import ReactiveSwift 18 | import Workflow 19 | import WorkflowUI 20 | 21 | // MARK: Input and Output 22 | 23 | struct RootWorkflow: Workflow { 24 | typealias Output = Never 25 | } 26 | 27 | // MARK: State and Initialization 28 | 29 | extension RootWorkflow { 30 | enum State { 31 | case welcome 32 | case demo(name: String) 33 | } 34 | 35 | func makeInitialState() -> RootWorkflow.State { 36 | .welcome 37 | } 38 | } 39 | 40 | // MARK: Actions 41 | 42 | extension RootWorkflow { 43 | enum Action: WorkflowAction { 44 | typealias WorkflowType = RootWorkflow 45 | 46 | case login(name: String) 47 | 48 | func apply(toState state: inout RootWorkflow.State, context: ApplyContext) -> RootWorkflow.Output? { 49 | switch self { 50 | case .login(name: let name): 51 | state = .demo(name: name) 52 | } 53 | 54 | return nil 55 | } 56 | } 57 | } 58 | 59 | // MARK: Rendering 60 | 61 | extension RootWorkflow { 62 | typealias Rendering = CrossFadeScreen 63 | 64 | func render(state: RootWorkflow.State, context: RenderContext) -> Rendering { 65 | switch state { 66 | case .welcome: 67 | CrossFadeScreen( 68 | base: WelcomeWorkflow() 69 | .mapOutput { output -> Action in 70 | switch output { 71 | case .login(name: let name): 72 | return .login(name: name) 73 | } 74 | } 75 | .rendered(in: context) 76 | ) 77 | 78 | case .demo(name: let name): 79 | CrossFadeScreen( 80 | base: DemoWorkflow(name: name) 81 | .rendered(in: context) 82 | ) 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Samples/SampleApp/Sources/WelcomeWorkflow.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import Workflow 18 | import WorkflowUI 19 | 20 | // MARK: Input and Output 21 | 22 | struct WelcomeWorkflow: Workflow { 23 | enum Output { 24 | case login(name: String) 25 | } 26 | } 27 | 28 | // MARK: State and Initialization 29 | 30 | extension WelcomeWorkflow { 31 | struct State { 32 | var name: String 33 | } 34 | 35 | func makeInitialState() -> WelcomeWorkflow.State { 36 | State(name: "") 37 | } 38 | } 39 | 40 | // MARK: Actions 41 | 42 | extension WelcomeWorkflow { 43 | enum Action: WorkflowAction { 44 | typealias WorkflowType = WelcomeWorkflow 45 | 46 | case nameChanged(String) 47 | case login 48 | 49 | func apply(toState state: inout WelcomeWorkflow.State, context: ApplyContext) -> WelcomeWorkflow.Output? { 50 | switch self { 51 | case .nameChanged(let updatedName): 52 | state.name = updatedName 53 | return nil 54 | 55 | case .login: 56 | return .login(name: state.name) 57 | } 58 | } 59 | } 60 | } 61 | 62 | // MARK: Rendering 63 | 64 | extension WelcomeWorkflow { 65 | typealias Rendering = WelcomeScreen 66 | 67 | func render(state: WelcomeWorkflow.State, context: RenderContext) -> Rendering { 68 | let sink = context.makeSink(of: Action.self) 69 | return WelcomeScreen( 70 | name: state.name, 71 | onNameChanged: { updatedName in 72 | sink.send(.nameChanged(updatedName)) 73 | }, 74 | onLoginTapped: { 75 | sink.send(.login) 76 | } 77 | ) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Samples/SnapshotTests/ReferenceImages_64/SplitScreenContainerScreenSnapshotTests/test_splitRatio_custom_iPad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/square/workflow-swift/53fc18d87e1785891b64f2dda782bae47f595399/Samples/SnapshotTests/ReferenceImages_64/SplitScreenContainerScreenSnapshotTests/test_splitRatio_custom_iPad.png -------------------------------------------------------------------------------- /Samples/SnapshotTests/ReferenceImages_64/SplitScreenContainerScreenSnapshotTests/test_splitRatio_half_iPad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/square/workflow-swift/53fc18d87e1785891b64f2dda782bae47f595399/Samples/SnapshotTests/ReferenceImages_64/SplitScreenContainerScreenSnapshotTests/test_splitRatio_half_iPad.png -------------------------------------------------------------------------------- /Samples/SnapshotTests/ReferenceImages_64/SplitScreenContainerScreenSnapshotTests/test_splitRatio_quarter_iPad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/square/workflow-swift/53fc18d87e1785891b64f2dda782bae47f595399/Samples/SnapshotTests/ReferenceImages_64/SplitScreenContainerScreenSnapshotTests/test_splitRatio_quarter_iPad.png -------------------------------------------------------------------------------- /Samples/SnapshotTests/ReferenceImages_64/SplitScreenContainerScreenSnapshotTests/test_splitRatio_third_iPad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/square/workflow-swift/53fc18d87e1785891b64f2dda782bae47f595399/Samples/SnapshotTests/ReferenceImages_64/SplitScreenContainerScreenSnapshotTests/test_splitRatio_third_iPad.png -------------------------------------------------------------------------------- /Samples/SplitScreenContainer/DemoApp/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import SplitScreenContainer 18 | import UIKit 19 | import Workflow 20 | import WorkflowUI 21 | 22 | @main 23 | class AppDelegate: UIResponder, UIApplicationDelegate { 24 | var window: UIWindow? 25 | 26 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 27 | let window = UIWindow(frame: UIScreen.main.bounds) 28 | 29 | let container = WorkflowHostingController( 30 | workflow: DemoWorkflow() 31 | ) 32 | 33 | window.rootViewController = container 34 | self.window = window 35 | window.makeKeyAndVisible() 36 | return true 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Samples/SplitScreenContainer/DemoApp/FooScreen.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import UIKit 18 | import Workflow 19 | import WorkflowUI 20 | 21 | struct FooScreen: Screen { 22 | let title: String 23 | let backgroundColor: UIColor 24 | let viewTapped: () -> Void 25 | 26 | func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { 27 | FooScreenViewController.description(for: self, environment: environment) 28 | } 29 | } 30 | 31 | private final class FooScreenViewController: ScreenViewController { 32 | private lazy var titleLabel: UILabel = .init() 33 | private lazy var tapGestureRecognizer: UITapGestureRecognizer = .init() 34 | 35 | override func viewDidLoad() { 36 | super.viewDidLoad() 37 | 38 | tapGestureRecognizer.addTarget(self, action: #selector(viewTapped)) 39 | view.addGestureRecognizer(tapGestureRecognizer) 40 | 41 | titleLabel.translatesAutoresizingMaskIntoConstraints = false 42 | titleLabel.textAlignment = .center 43 | view.addSubview(titleLabel) 44 | 45 | NSLayoutConstraint.activate([ 46 | titleLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor), 47 | titleLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor), 48 | ]) 49 | } 50 | 51 | override func screenDidChange(from previousScreen: FooScreen, previousEnvironment: ViewEnvironment) { 52 | super.screenDidChange(from: previousScreen, previousEnvironment: previousEnvironment) 53 | 54 | view.backgroundColor = screen.backgroundColor 55 | titleLabel.text = screen.title 56 | } 57 | 58 | @objc 59 | private func viewTapped() { 60 | screen.viewTapped() 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Samples/SplitScreenContainer/README.md: -------------------------------------------------------------------------------- 1 | # SplitScreenContainer 2 | 3 | Container to display two screens side by side. 4 | -------------------------------------------------------------------------------- /Samples/SplitScreenContainer/Sources/ContainerView.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import UIKit 18 | 19 | class ContainerView: UIView { 20 | var contentView: UIView = .init() 21 | 22 | override init(frame: CGRect) { 23 | super.init(frame: frame) 24 | 25 | commonInit() 26 | } 27 | 28 | required init?(coder: NSCoder) { 29 | super.init(coder: coder) 30 | 31 | commonInit() 32 | } 33 | 34 | private func commonInit() { 35 | addSubview(contentView) 36 | } 37 | 38 | override func layoutSubviews() { 39 | super.layoutSubviews() 40 | 41 | contentView.frame = bounds 42 | 43 | contentView.subviews.forEach { $0.frame = self.bounds } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Samples/SplitScreenContainer/Sources/Environment+SplitScreen.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import WorkflowUI 18 | 19 | public enum SplitScreenPosition { 20 | /// Not appearing in a split screen context 21 | case none 22 | 23 | /// Appearing in the leading position in a split screen 24 | case leading 25 | 26 | /// Appearing in the trailing position in a split screen 27 | case trailing 28 | } 29 | 30 | extension ViewEnvironment { 31 | public internal(set) var splitScreenPosition: SplitScreenPosition { 32 | get { self[SplitScreenPositionKey.self] } 33 | set { self[SplitScreenPositionKey.self] = newValue } 34 | } 35 | } 36 | 37 | private enum SplitScreenPositionKey: ViewEnvironmentKey { 38 | static var defaultValue: SplitScreenPosition = .none 39 | } 40 | -------------------------------------------------------------------------------- /Samples/SplitScreenContainer/Sources/SplitScreenContainerScreen.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import UIKit 18 | import WorkflowUI 19 | 20 | /// A `SplitScreenContainerScreen` displays two screens side by side with a separator in between. 21 | public struct SplitScreenContainerScreen: Screen { 22 | /// The screen displayed leading the separator. 23 | public let leadingScreen: LeadingScreenType 24 | 25 | /// The screen displayed trailing the separator. 26 | public let trailingScreen: TrailingScreenType 27 | 28 | /// The ratio of `leadingScreen`'s width relative to that of `trailingScreen`. Defaults to `.third`. 29 | public let ratio: CGFloat 30 | 31 | /// The color of the `separatorView` displayed between `leadingScreen`'s and `trailingScreen`'s views. 32 | public let separatorColor: UIColor 33 | 34 | /// The width of the `separatorView` displayed between `leadingScreen`'s and `trailingScreen`'s views. 35 | public let separatorWidth: CGFloat 36 | 37 | public init( 38 | leadingScreen: LeadingScreenType, 39 | trailingScreen: TrailingScreenType, 40 | ratio: CGFloat = .third, 41 | separatorColor: UIColor = .black, 42 | separatorWidth: CGFloat = 1.0 43 | ) { 44 | self.leadingScreen = leadingScreen 45 | self.trailingScreen = trailingScreen 46 | self.ratio = ratio 47 | self.separatorColor = separatorColor 48 | self.separatorWidth = separatorWidth 49 | } 50 | 51 | public func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { 52 | SplitScreenContainerViewController.description(for: self, environment: environment) 53 | } 54 | } 55 | 56 | extension CGFloat { 57 | public static let quarter: CGFloat = 1.0 / 4.0 58 | public static let third: CGFloat = 1.0 / 3.0 59 | public static let half: CGFloat = 1.0 / 2.0 60 | } 61 | -------------------------------------------------------------------------------- /Samples/TicTacToe/.gitignore: -------------------------------------------------------------------------------- 1 | Podfile.lock 2 | -------------------------------------------------------------------------------- /Samples/TicTacToe/Sources/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import BackStackContainer 18 | import UIKit 19 | import WorkflowUI 20 | 21 | @main 22 | class AppDelegate: UIResponder, UIApplicationDelegate { 23 | var window: UIWindow? 24 | 25 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 26 | window = UIWindow(frame: UIScreen.main.bounds) 27 | 28 | window?.rootViewController = WorkflowHostingController(workflow: MainWorkflow()) 29 | 30 | window?.makeKeyAndVisible() 31 | 32 | return true 33 | } 34 | 35 | func applicationWillResignActive(_ application: UIApplication) {} 36 | 37 | func applicationDidEnterBackground(_ application: UIApplication) {} 38 | 39 | func applicationWillEnterForeground(_ application: UIApplication) {} 40 | 41 | func applicationDidBecomeActive(_ application: UIApplication) {} 42 | 43 | func applicationWillTerminate(_ application: UIApplication) {} 44 | } 45 | -------------------------------------------------------------------------------- /Samples/TicTacToe/Sources/Authentication/LoadingScreen.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import UIKit 18 | import WorkflowUI 19 | 20 | struct LoadingScreen: Screen { 21 | func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { 22 | LoadingScreenViewController.description(for: self, environment: environment) 23 | } 24 | } 25 | 26 | private final class LoadingScreenViewController: ScreenViewController { 27 | let loadingLabel = UILabel(frame: .zero) 28 | 29 | override func viewDidLoad() { 30 | super.viewDidLoad() 31 | 32 | loadingLabel.font = UIFont.boldSystemFont(ofSize: 44.0) 33 | loadingLabel.textColor = .black 34 | loadingLabel.textAlignment = .center 35 | loadingLabel.text = "Loading..." 36 | 37 | view.backgroundColor = .white 38 | 39 | view.addSubview(loadingLabel) 40 | } 41 | 42 | override func viewDidLayoutSubviews() { 43 | super.viewDidLayoutSubviews() 44 | 45 | loadingLabel.frame = view.bounds 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Samples/TicTacToe/Sources/Game/GameState.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | enum GameState: Equatable { 18 | case ongoing(turn: Player) 19 | case win(Player) 20 | case tie 21 | 22 | mutating func toggle() { 23 | switch self { 24 | case .ongoing(turn: let player): 25 | switch player { 26 | case .x: 27 | self = .ongoing(turn: .o) 28 | case .o: 29 | self = .ongoing(turn: .x) 30 | } 31 | default: 32 | break 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Samples/TicTacToe/Tests/MainWorkflowTests.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import BackStackContainer 18 | import ModalContainer 19 | import Workflow 20 | import WorkflowTesting 21 | import XCTest 22 | 23 | @testable import TicTacToe 24 | 25 | class MainWorkflowTests: XCTestCase { 26 | // MARK: Action Tests 27 | 28 | func test_action_authenticated() { 29 | MainWorkflow 30 | .Action 31 | .tester(withState: .authenticating) 32 | .send(action: .authenticated(sessionToken: "token")) 33 | .verifyState { state in 34 | if case MainWorkflow.State.runningGame(let token) = state { 35 | XCTAssertEqual(token, "token") 36 | } else { 37 | XCTFail("Invalid state after authenticated") 38 | } 39 | } 40 | } 41 | 42 | func test_action_logout() { 43 | MainWorkflow 44 | .Action 45 | .tester(withState: .runningGame(sessionToken: "token")) 46 | .send(action: .logout) 47 | .assert(state: .authenticating) 48 | } 49 | 50 | // MARK: Render Tests 51 | 52 | func test_render_authenticating() { 53 | MainWorkflow() 54 | .renderTester() 55 | .expectWorkflow( 56 | type: AuthenticationWorkflow.self, 57 | producingRendering: AuthenticationWorkflow.Rendering( 58 | baseScreen: ModalContainerScreen( 59 | baseScreen: BackStackScreen(items: []), modals: [] 60 | ), 61 | alert: nil 62 | ) 63 | ) 64 | .render { screen in 65 | XCTAssertNil(screen.alert) 66 | } 67 | .assertNoAction() 68 | } 69 | 70 | func disabled_test_render_runningGame() { 71 | MainWorkflow() 72 | .renderTester() 73 | .render { screen in 74 | XCTAssertNil(screen.alert) 75 | } 76 | .assert(state: .runningGame(sessionToken: "token")) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Samples/Tuist/Config.swift: -------------------------------------------------------------------------------- 1 | import ProjectDescription 2 | 3 | let config = Config( 4 | // This breaks snapshot tests, because iOSSnapshotTestCase depends on XCTest. 5 | // ENABLE_TESTING_SEARCH_PATHS should sufficient but doesn't seem to work with 6 | // enforceExplicitDependencies enabled. 7 | // generationOptions: .options(enforceExplicitDependencies: true) 8 | ) 9 | -------------------------------------------------------------------------------- /Samples/Tuist/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | 3 | import PackageDescription 4 | 5 | #if TUIST 6 | import ProjectDescription 7 | 8 | let unsuppressedWarningsSettings: SettingsDictionary = [ 9 | "GCC_WARN_INHIBIT_ALL_WARNINGS": "$(inherited)", 10 | "SWIFT_SUPPRESS_WARNINGS": "$(inherited)", 11 | ] 12 | 13 | let packageSettings = PackageSettings( 14 | productTypes: [ 15 | "iOSSnapshotTestCase": .framework, 16 | "ReactiveSwift": .framework, 17 | "ViewEnvironment": .framework, 18 | "ViewEnvironmentUI": .framework, 19 | "Workflow": .framework, 20 | "WorkflowReactiveSwift": .framework, 21 | "WorkflowUI": .framework, 22 | ], 23 | targetSettings: [ 24 | "iOSSnapshotTestCase": ["ENABLE_TESTING_SEARCH_PATHS": "YES"], 25 | "ViewEnvironment": unsuppressedWarningsSettings, 26 | "ViewEnvironmentUI": unsuppressedWarningsSettings, 27 | "Workflow": unsuppressedWarningsSettings, 28 | "WorkflowCombine": unsuppressedWarningsSettings, 29 | "WorkflowCombineTesting": unsuppressedWarningsSettings, 30 | "WorkflowConcurrency": unsuppressedWarningsSettings, 31 | "WorkflowConcurrencyTesting": unsuppressedWarningsSettings, 32 | "WorkflowReactiveSwift": unsuppressedWarningsSettings, 33 | "WorkflowReactiveSwiftTesting": unsuppressedWarningsSettings, 34 | "WorkflowRxSwift": unsuppressedWarningsSettings, 35 | "WorkflowRxSwiftTesting": unsuppressedWarningsSettings, 36 | "WorkflowTesting": unsuppressedWarningsSettings, 37 | "WorkflowUI": unsuppressedWarningsSettings, 38 | ] 39 | ) 40 | 41 | #endif 42 | 43 | let package = Package( 44 | name: "WorkflowDevelopment", 45 | dependencies: [ 46 | .package(path: "../../"), 47 | .package(url: "https://github.com/uber/ios-snapshot-test-case.git", from: "8.0.0"), 48 | ] 49 | ) 50 | -------------------------------------------------------------------------------- /Samples/Tutorial/.gitignore: -------------------------------------------------------------------------------- 1 | /Tutorial.xcodeproj 2 | /Tutorial.xcworkspace 3 | -------------------------------------------------------------------------------- /Samples/Tutorial/AppHost/Sources/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import TutorialBase 18 | import UIKit 19 | 20 | @main 21 | class AppDelegate: UIResponder, UIApplicationDelegate { 22 | var window: UIWindow? 23 | 24 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 25 | window = UIWindow(frame: UIScreen.main.bounds) 26 | 27 | let viewController = TutorialHostingViewController() 28 | 29 | window?.rootViewController = viewController 30 | 31 | window?.makeKeyAndVisible() 32 | 33 | return true 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Samples/Tutorial/Frameworks/Tutorial1Complete/Sources/Todo/Edit/TodoEditSampleViewController.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import TutorialViews 18 | import UIKit 19 | 20 | final class TodoEditSampleViewController: UIViewController { 21 | let todoEditView: TodoEditView 22 | 23 | init() { 24 | self.todoEditView = TodoEditView(frame: .zero) 25 | 26 | super.init(nibName: nil, bundle: nil) 27 | } 28 | 29 | @available(*, unavailable) 30 | required init?(coder aDecoder: NSCoder) { 31 | fatalError("init(coder:) has not been implemented") 32 | } 33 | 34 | // MARK: UIViewController 35 | 36 | override func viewDidLoad() { 37 | super.viewDidLoad() 38 | 39 | view.addSubview(todoEditView) 40 | } 41 | 42 | override func viewDidLayoutSubviews() { 43 | super.viewDidLayoutSubviews() 44 | 45 | todoEditView.frame = view.bounds.inset(by: view.safeAreaInsets) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Samples/Tutorial/Frameworks/Tutorial1Complete/Sources/Todo/List/TodoListSampleViewController.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import TutorialViews 18 | import UIKit 19 | 20 | final class TodoListSampleViewController: UIViewController { 21 | let todoListView: TodoListView 22 | 23 | init() { 24 | self.todoListView = TodoListView(frame: .zero) 25 | 26 | super.init(nibName: nil, bundle: nil) 27 | } 28 | 29 | @available(*, unavailable) 30 | required init?(coder aDecoder: NSCoder) { 31 | fatalError("init(coder:) has not been implemented") 32 | } 33 | 34 | // MARK: UIViewController 35 | 36 | override func viewDidLoad() { 37 | super.viewDidLoad() 38 | 39 | view.addSubview(todoListView) 40 | } 41 | 42 | override func viewDidLayoutSubviews() { 43 | super.viewDidLayoutSubviews() 44 | 45 | todoListView.frame = view.bounds.inset(by: view.safeAreaInsets) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Samples/Tutorial/Frameworks/Tutorial1Complete/Sources/Todo/Model/TodoModel.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | struct TodoModel: Equatable { 18 | var title: String 19 | var note: String 20 | } 21 | -------------------------------------------------------------------------------- /Samples/Tutorial/Frameworks/Tutorial1Complete/Sources/TutorialHostingViewController.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import UIKit 18 | import Workflow 19 | import WorkflowUI 20 | 21 | public final class TutorialHostingViewController: UIViewController { 22 | let containerViewController: UIViewController 23 | 24 | public init() { 25 | // Create a `WorkflowHostingController` with the `WelcomeWorkflow` as the root workflow 26 | self.containerViewController = WorkflowHostingController(workflow: WelcomeWorkflow()) 27 | 28 | super.init(nibName: nil, bundle: nil) 29 | } 30 | 31 | @available(*, unavailable) 32 | required init?(coder aDecoder: NSCoder) { 33 | fatalError("init(coder:) has not been implemented") 34 | } 35 | 36 | override public func viewDidLoad() { 37 | super.viewDidLoad() 38 | 39 | view.backgroundColor = .white 40 | 41 | addChild(containerViewController) 42 | view.addSubview(containerViewController.view) 43 | containerViewController.didMove(toParent: self) 44 | } 45 | 46 | override public func viewDidLayoutSubviews() { 47 | super.viewDidLayoutSubviews() 48 | 49 | containerViewController.view.frame = view.bounds 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Samples/Tutorial/Frameworks/Tutorial1Complete/Sources/Welcome/WelcomeScreen.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import TutorialViews 18 | import Workflow 19 | import WorkflowUI 20 | 21 | struct WelcomeScreen: Screen { 22 | /// The current name that has been entered. 23 | var name: String 24 | /// Callback when the name changes in the UI. 25 | var onNameChanged: (String) -> Void 26 | /// Callback when the login button is tapped. 27 | var onLoginTapped: () -> Void 28 | 29 | func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { 30 | WelcomeViewController.description(for: self, environment: environment) 31 | } 32 | } 33 | 34 | final class WelcomeViewController: ScreenViewController { 35 | private var welcomeView: WelcomeView! 36 | 37 | required init(screen: WelcomeScreen, environment: ViewEnvironment) { 38 | super.init(screen: screen, environment: environment) 39 | } 40 | 41 | override func viewDidLoad() { 42 | super.viewDidLoad() 43 | 44 | welcomeView = WelcomeView(frame: view.bounds) 45 | updateView(with: screen) 46 | 47 | view.addSubview(welcomeView) 48 | } 49 | 50 | override func viewDidLayoutSubviews() { 51 | super.viewDidLayoutSubviews() 52 | 53 | welcomeView.frame = view.bounds.inset(by: view.safeAreaInsets) 54 | } 55 | 56 | override func screenDidChange(from previousScreen: WelcomeScreen, previousEnvironment: ViewEnvironment) { 57 | super.screenDidChange(from: previousScreen, previousEnvironment: previousEnvironment) 58 | 59 | guard isViewLoaded else { return } 60 | 61 | updateView(with: screen) 62 | } 63 | 64 | private func updateView(with screen: WelcomeScreen) { 65 | welcomeView.name = screen.name 66 | welcomeView.onNameChanged = screen.onNameChanged 67 | welcomeView.onLoginTapped = screen.onLoginTapped 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Samples/Tutorial/Frameworks/Tutorial2Complete/Sources/Todo/Edit/TodoEditSampleViewController.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import TutorialViews 18 | import UIKit 19 | 20 | final class TodoEditSampleViewController: UIViewController { 21 | let todoEditView: TodoEditView 22 | 23 | init() { 24 | self.todoEditView = TodoEditView(frame: .zero) 25 | 26 | super.init(nibName: nil, bundle: nil) 27 | } 28 | 29 | @available(*, unavailable) 30 | required init?(coder aDecoder: NSCoder) { 31 | fatalError("init(coder:) has not been implemented") 32 | } 33 | 34 | // MARK: UIViewController 35 | 36 | override func viewDidLoad() { 37 | super.viewDidLoad() 38 | 39 | view.addSubview(todoEditView) 40 | } 41 | 42 | override func viewDidLayoutSubviews() { 43 | super.viewDidLayoutSubviews() 44 | 45 | todoEditView.frame = view.bounds.inset(by: view.safeAreaInsets) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Samples/Tutorial/Frameworks/Tutorial2Complete/Sources/Todo/List/TodoListScreen.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import TutorialViews 18 | import Workflow 19 | import WorkflowUI 20 | 21 | struct TodoListScreen: Screen { 22 | // The titles of the todo items 23 | var todoTitles: [String] 24 | 25 | // Callback when a todo is selected 26 | var onTodoSelected: (Int) -> Void 27 | 28 | func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { 29 | TodoListViewController.description(for: self, environment: environment) 30 | } 31 | } 32 | 33 | final class TodoListViewController: ScreenViewController { 34 | private var todoListView: TodoListView! 35 | 36 | required init(screen: TodoListScreen, environment: ViewEnvironment) { 37 | super.init(screen: screen, environment: environment) 38 | } 39 | 40 | override func viewDidLoad() { 41 | super.viewDidLoad() 42 | 43 | todoListView = TodoListView(frame: view.bounds) 44 | view.addSubview(todoListView) 45 | 46 | updateView(with: screen) 47 | } 48 | 49 | override func viewDidLayoutSubviews() { 50 | super.viewDidLayoutSubviews() 51 | 52 | todoListView.frame = view.bounds.inset(by: view.safeAreaInsets) 53 | } 54 | 55 | override func screenDidChange(from previousScreen: TodoListScreen, previousEnvironment: ViewEnvironment) { 56 | super.screenDidChange(from: previousScreen, previousEnvironment: previousEnvironment) 57 | 58 | guard isViewLoaded else { return } 59 | 60 | updateView(with: screen) 61 | } 62 | 63 | private func updateView(with screen: TodoListScreen) { 64 | // Update the todoList on the view with what the screen provided: 65 | todoListView.todoList = screen.todoTitles 66 | todoListView.onTodoSelected = screen.onTodoSelected 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Samples/Tutorial/Frameworks/Tutorial2Complete/Sources/Todo/Model/TodoModel.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | struct TodoModel: Equatable { 18 | var title: String 19 | var note: String 20 | } 21 | -------------------------------------------------------------------------------- /Samples/Tutorial/Frameworks/Tutorial2Complete/Sources/TutorialHostingViewController.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import BackStackContainer 18 | import UIKit 19 | import Workflow 20 | import WorkflowUI 21 | 22 | public final class TutorialHostingViewController: UIViewController { 23 | let containerViewController: UIViewController 24 | 25 | public init() { 26 | // Create a `WorkflowHostingController` with the `RootWorkflow` as the root workflow 27 | self.containerViewController = WorkflowHostingController(workflow: RootWorkflow()) 28 | 29 | super.init(nibName: nil, bundle: nil) 30 | } 31 | 32 | @available(*, unavailable) 33 | required init?(coder aDecoder: NSCoder) { 34 | fatalError("init(coder:) has not been implemented") 35 | } 36 | 37 | override public func viewDidLoad() { 38 | super.viewDidLoad() 39 | 40 | view.backgroundColor = .white 41 | 42 | addChild(containerViewController) 43 | view.addSubview(containerViewController.view) 44 | containerViewController.didMove(toParent: self) 45 | } 46 | 47 | override public func viewDidLayoutSubviews() { 48 | super.viewDidLayoutSubviews() 49 | 50 | containerViewController.view.frame = view.bounds 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Samples/Tutorial/Frameworks/Tutorial2Complete/Sources/Welcome/WelcomeScreen.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import TutorialViews 18 | import Workflow 19 | import WorkflowUI 20 | 21 | struct WelcomeScreen: Screen { 22 | /// The current name that has been entered. 23 | var name: String 24 | /// Callback when the name changes in the UI. 25 | var onNameChanged: (String) -> Void 26 | /// Callback when the login button is tapped. 27 | var onLoginTapped: () -> Void 28 | 29 | func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { 30 | WelcomeViewController.description(for: self, environment: environment) 31 | } 32 | } 33 | 34 | final class WelcomeViewController: ScreenViewController { 35 | private var welcomeView: WelcomeView! 36 | 37 | required init(screen: WelcomeScreen, environment: ViewEnvironment) { 38 | super.init(screen: screen, environment: environment) 39 | } 40 | 41 | override func viewDidLoad() { 42 | super.viewDidLoad() 43 | 44 | welcomeView = WelcomeView(frame: view.bounds) 45 | updateView(with: screen) 46 | 47 | view.addSubview(welcomeView) 48 | } 49 | 50 | override func viewDidLayoutSubviews() { 51 | super.viewDidLayoutSubviews() 52 | 53 | welcomeView.frame = view.bounds.inset(by: view.safeAreaInsets) 54 | } 55 | 56 | override func screenDidChange(from previousScreen: WelcomeScreen, previousEnvironment: ViewEnvironment) { 57 | super.screenDidChange(from: previousScreen, previousEnvironment: previousEnvironment) 58 | 59 | guard isViewLoaded else { return } 60 | 61 | updateView(with: screen) 62 | } 63 | 64 | private func updateView(with screen: WelcomeScreen) { 65 | welcomeView.name = screen.name 66 | welcomeView.onNameChanged = screen.onNameChanged 67 | welcomeView.onLoginTapped = screen.onLoginTapped 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Samples/Tutorial/Frameworks/Tutorial3Complete/Sources/Todo/Edit/TodoEditScreen.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import TutorialViews 18 | import Workflow 19 | import WorkflowUI 20 | 21 | struct TodoEditScreen: Screen { 22 | // The title of this todo item. 23 | var title: String 24 | // The contents, or "note" of the todo. 25 | var note: String 26 | 27 | // Callback for when the title or note changes 28 | var onTitleChanged: (String) -> Void 29 | var onNoteChanged: (String) -> Void 30 | 31 | func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { 32 | TodoEditViewController.description(for: self, environment: environment) 33 | } 34 | } 35 | 36 | final class TodoEditViewController: ScreenViewController { 37 | private var todoEditView: TodoEditView! 38 | 39 | required init(screen: TodoEditScreen, environment: ViewEnvironment) { 40 | super.init(screen: screen, environment: environment) 41 | } 42 | 43 | override func viewDidLoad() { 44 | super.viewDidLoad() 45 | 46 | todoEditView = TodoEditView(frame: view.bounds) 47 | view.addSubview(todoEditView) 48 | 49 | updateView(with: screen) 50 | } 51 | 52 | override func viewDidLayoutSubviews() { 53 | super.viewDidLayoutSubviews() 54 | 55 | todoEditView.frame = view.bounds.inset(by: view.safeAreaInsets) 56 | } 57 | 58 | override func screenDidChange(from previousScreen: TodoEditScreen, previousEnvironment: ViewEnvironment) { 59 | super.screenDidChange(from: previousScreen, previousEnvironment: previousEnvironment) 60 | 61 | guard isViewLoaded else { return } 62 | 63 | updateView(with: screen) 64 | } 65 | 66 | private func updateView(with screen: TodoEditScreen) { 67 | // Update the view with the data from the screen. 68 | todoEditView.title = screen.title 69 | todoEditView.note = screen.note 70 | todoEditView.onTitleChanged = screen.onTitleChanged 71 | todoEditView.onNoteChanged = screen.onNoteChanged 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Samples/Tutorial/Frameworks/Tutorial3Complete/Sources/Todo/List/TodoListScreen.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import TutorialViews 18 | import Workflow 19 | import WorkflowUI 20 | 21 | struct TodoListScreen: Screen { 22 | // The titles of the todo items 23 | var todoTitles: [String] 24 | 25 | // Callback when a todo is selected 26 | var onTodoSelected: (Int) -> Void 27 | 28 | func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { 29 | TodoListViewController.description(for: self, environment: environment) 30 | } 31 | } 32 | 33 | final class TodoListViewController: ScreenViewController { 34 | private var todoListView: TodoListView! 35 | 36 | required init(screen: TodoListScreen, environment: ViewEnvironment) { 37 | super.init(screen: screen, environment: environment) 38 | } 39 | 40 | override func viewDidLoad() { 41 | super.viewDidLoad() 42 | 43 | todoListView = TodoListView(frame: view.bounds) 44 | view.addSubview(todoListView) 45 | 46 | updateView(with: screen) 47 | } 48 | 49 | override func viewDidLayoutSubviews() { 50 | super.viewDidLayoutSubviews() 51 | 52 | todoListView.frame = view.bounds.inset(by: view.safeAreaInsets) 53 | } 54 | 55 | override func screenDidChange(from previousScreen: TodoListScreen, previousEnvironment: ViewEnvironment) { 56 | super.screenDidChange(from: previousScreen, previousEnvironment: previousEnvironment) 57 | 58 | guard isViewLoaded else { return } 59 | 60 | updateView(with: screen) 61 | } 62 | 63 | private func updateView(with screen: TodoListScreen) { 64 | // Update the todoList on the view with what the screen provided: 65 | todoListView.todoList = screen.todoTitles 66 | todoListView.onTodoSelected = screen.onTodoSelected 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Samples/Tutorial/Frameworks/Tutorial3Complete/Sources/Todo/Model/TodoModel.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | struct TodoModel: Equatable { 18 | var title: String 19 | var note: String 20 | } 21 | -------------------------------------------------------------------------------- /Samples/Tutorial/Frameworks/Tutorial3Complete/Sources/TutorialHostingViewController.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import BackStackContainer 18 | import UIKit 19 | import Workflow 20 | import WorkflowUI 21 | 22 | public final class TutorialHostingViewController: UIViewController { 23 | let containerViewController: UIViewController 24 | 25 | public init() { 26 | // Create a `WorkflowHostingController` with the `RootWorkflow` as the root workflow 27 | self.containerViewController = WorkflowHostingController(workflow: RootWorkflow()) 28 | 29 | super.init(nibName: nil, bundle: nil) 30 | } 31 | 32 | @available(*, unavailable) 33 | required init?(coder aDecoder: NSCoder) { 34 | fatalError("init(coder:) has not been implemented") 35 | } 36 | 37 | override public func viewDidLoad() { 38 | super.viewDidLoad() 39 | 40 | view.backgroundColor = .white 41 | 42 | addChild(containerViewController) 43 | view.addSubview(containerViewController.view) 44 | containerViewController.didMove(toParent: self) 45 | } 46 | 47 | override public func viewDidLayoutSubviews() { 48 | super.viewDidLayoutSubviews() 49 | 50 | containerViewController.view.frame = view.bounds 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Samples/Tutorial/Frameworks/Tutorial3Complete/Sources/Welcome/WelcomeScreen.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import TutorialViews 18 | import Workflow 19 | import WorkflowUI 20 | 21 | struct WelcomeScreen: Screen { 22 | /// The current name that has been entered. 23 | var name: String 24 | /// Callback when the name changes in the UI. 25 | var onNameChanged: (String) -> Void 26 | /// Callback when the login button is tapped. 27 | var onLoginTapped: () -> Void 28 | 29 | func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { 30 | WelcomeViewController.description(for: self, environment: environment) 31 | } 32 | } 33 | 34 | final class WelcomeViewController: ScreenViewController { 35 | private var welcomeView: WelcomeView! 36 | 37 | required init(screen: WelcomeScreen, environment: ViewEnvironment) { 38 | super.init(screen: screen, environment: environment) 39 | } 40 | 41 | override func viewDidLoad() { 42 | super.viewDidLoad() 43 | 44 | welcomeView = WelcomeView(frame: view.bounds) 45 | updateView(with: screen) 46 | 47 | view.addSubview(welcomeView) 48 | } 49 | 50 | override func viewDidLayoutSubviews() { 51 | super.viewDidLayoutSubviews() 52 | 53 | welcomeView.frame = view.bounds.inset(by: view.safeAreaInsets) 54 | } 55 | 56 | override func screenDidChange(from previousScreen: WelcomeScreen, previousEnvironment: ViewEnvironment) { 57 | super.screenDidChange(from: previousScreen, previousEnvironment: previousEnvironment) 58 | 59 | guard isViewLoaded else { return } 60 | 61 | updateView(with: screen) 62 | } 63 | 64 | private func updateView(with screen: WelcomeScreen) { 65 | welcomeView.name = screen.name 66 | welcomeView.onNameChanged = screen.onNameChanged 67 | welcomeView.onLoginTapped = screen.onLoginTapped 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Samples/Tutorial/Frameworks/Tutorial4Complete/Sources/Todo/Edit/TodoEditScreen.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import TutorialViews 18 | import Workflow 19 | import WorkflowUI 20 | 21 | struct TodoEditScreen: Screen { 22 | // The title of this todo item. 23 | var title: String 24 | // The contents, or "note" of the todo. 25 | var note: String 26 | 27 | // Callback for when the title or note changes 28 | var onTitleChanged: (String) -> Void 29 | var onNoteChanged: (String) -> Void 30 | 31 | func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { 32 | TodoEditViewController.description(for: self, environment: environment) 33 | } 34 | } 35 | 36 | final class TodoEditViewController: ScreenViewController { 37 | // The `todoEditView` has all the logic for displaying the todo and editing. 38 | let todoEditView: TodoEditView 39 | 40 | required init(screen: TodoEditScreen, environment: ViewEnvironment) { 41 | self.todoEditView = TodoEditView(frame: .zero) 42 | 43 | super.init(screen: screen, environment: environment) 44 | update(with: screen) 45 | } 46 | 47 | override func viewDidLoad() { 48 | super.viewDidLoad() 49 | 50 | view.addSubview(todoEditView) 51 | } 52 | 53 | override func viewDidLayoutSubviews() { 54 | super.viewDidLayoutSubviews() 55 | 56 | todoEditView.frame = view.bounds.inset(by: view.safeAreaInsets) 57 | } 58 | 59 | override func screenDidChange(from previousScreen: TodoEditScreen, previousEnvironment: ViewEnvironment) { 60 | update(with: screen) 61 | } 62 | 63 | private func update(with screen: TodoEditScreen) { 64 | // Update the view with the data from the screen. 65 | todoEditView.title = screen.title 66 | todoEditView.note = screen.note 67 | todoEditView.onTitleChanged = screen.onTitleChanged 68 | todoEditView.onNoteChanged = screen.onNoteChanged 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Samples/Tutorial/Frameworks/Tutorial4Complete/Sources/Todo/List/TodoListScreen.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import TutorialViews 18 | import Workflow 19 | import WorkflowUI 20 | 21 | struct TodoListScreen: Screen { 22 | // The titles of the todo items 23 | var todoTitles: [String] 24 | 25 | // Callback when a todo is selected 26 | var onTodoSelected: (Int) -> Void 27 | 28 | func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { 29 | TodoListViewController.description(for: self, environment: environment) 30 | } 31 | } 32 | 33 | final class TodoListViewController: ScreenViewController { 34 | let todoListView: TodoListView 35 | 36 | required init(screen: TodoListScreen, environment: ViewEnvironment) { 37 | self.todoListView = TodoListView(frame: .zero) 38 | super.init(screen: screen, environment: environment) 39 | update(with: screen) 40 | } 41 | 42 | override func viewDidLoad() { 43 | super.viewDidLoad() 44 | 45 | view.addSubview(todoListView) 46 | } 47 | 48 | override func viewDidLayoutSubviews() { 49 | super.viewDidLayoutSubviews() 50 | 51 | todoListView.frame = view.bounds.inset(by: view.safeAreaInsets) 52 | } 53 | 54 | override func screenDidChange(from previousScreen: TodoListScreen, previousEnvironment: ViewEnvironment) { 55 | update(with: screen) 56 | } 57 | 58 | private func update(with screen: TodoListScreen) { 59 | // Update the todoList on the view with what the screen provided: 60 | todoListView.todoList = screen.todoTitles 61 | todoListView.onTodoSelected = screen.onTodoSelected 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Samples/Tutorial/Frameworks/Tutorial4Complete/Sources/Todo/Model/TodoModel.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | struct TodoModel: Equatable { 18 | var title: String 19 | var note: String 20 | } 21 | -------------------------------------------------------------------------------- /Samples/Tutorial/Frameworks/Tutorial4Complete/Sources/TutorialHostingViewController.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import BackStackContainer 18 | import UIKit 19 | import Workflow 20 | import WorkflowUI 21 | 22 | public final class TutorialHostingViewController: UIViewController { 23 | let containerViewController: UIViewController 24 | 25 | public init() { 26 | // Create a `WorkflowHostingController` with the `RootWorkflow` as the root workflow 27 | self.containerViewController = WorkflowHostingController(workflow: RootWorkflow()) 28 | 29 | super.init(nibName: nil, bundle: nil) 30 | } 31 | 32 | @available(*, unavailable) 33 | required init?(coder aDecoder: NSCoder) { 34 | fatalError("init(coder:) has not been implemented") 35 | } 36 | 37 | override public func viewDidLoad() { 38 | super.viewDidLoad() 39 | 40 | view.backgroundColor = .white 41 | 42 | addChild(containerViewController) 43 | view.addSubview(containerViewController.view) 44 | containerViewController.didMove(toParent: self) 45 | } 46 | 47 | override public func viewDidLayoutSubviews() { 48 | super.viewDidLayoutSubviews() 49 | 50 | containerViewController.view.frame = view.bounds 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Samples/Tutorial/Frameworks/Tutorial4Complete/Sources/Welcome/WelcomeScreen.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import TutorialViews 18 | import Workflow 19 | import WorkflowUI 20 | 21 | struct WelcomeScreen: Screen { 22 | /// The current name that has been entered. 23 | var name: String 24 | /// Callback when the name changes in the UI. 25 | var onNameChanged: (String) -> Void 26 | /// Callback when the login button is tapped. 27 | var onLoginTapped: () -> Void 28 | 29 | func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { 30 | WelcomeViewController.description(for: self, environment: environment) 31 | } 32 | } 33 | 34 | final class WelcomeViewController: ScreenViewController { 35 | var welcomeView: WelcomeView 36 | 37 | required init(screen: WelcomeScreen, environment: ViewEnvironment) { 38 | self.welcomeView = WelcomeView(frame: .zero) 39 | super.init(screen: screen, environment: environment) 40 | update(with: screen) 41 | } 42 | 43 | override func viewDidLoad() { 44 | super.viewDidLoad() 45 | 46 | view.addSubview(welcomeView) 47 | } 48 | 49 | override func viewDidLayoutSubviews() { 50 | super.viewDidLayoutSubviews() 51 | 52 | welcomeView.frame = view.bounds.inset(by: view.safeAreaInsets) 53 | } 54 | 55 | override func screenDidChange(from previousScreen: WelcomeScreen, previousEnvironment: ViewEnvironment) { 56 | update(with: screen) 57 | } 58 | 59 | private func update(with screen: WelcomeScreen) { 60 | /// Update UI 61 | welcomeView.name = screen.name 62 | welcomeView.onNameChanged = screen.onNameChanged 63 | welcomeView.onLoginTapped = screen.onLoginTapped 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Samples/Tutorial/Frameworks/Tutorial4Complete/Tests/TutorialTests.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import XCTest 18 | 19 | final class TutorialTests: XCTestCase { 20 | func testPlaceholder() { 21 | XCTAssertEqual(1, 1, "Placeholder test") 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Samples/Tutorial/Frameworks/Tutorial5Complete/Sources/Todo/List/TodoListScreen.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import TutorialViews 18 | import Workflow 19 | import WorkflowUI 20 | 21 | struct TodoListScreen: Screen { 22 | // The titles of the todo items 23 | var todoTitles: [String] 24 | 25 | // Callback when a todo is selected 26 | var onTodoSelected: (Int) -> Void 27 | 28 | func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { 29 | TodoListViewController.description(for: self, environment: environment) 30 | } 31 | } 32 | 33 | final class TodoListViewController: ScreenViewController { 34 | private var todoListView: TodoListView! 35 | 36 | required init(screen: TodoListScreen, environment: ViewEnvironment) { 37 | super.init(screen: screen, environment: environment) 38 | } 39 | 40 | override func viewDidLoad() { 41 | super.viewDidLoad() 42 | 43 | todoListView = TodoListView(frame: view.bounds) 44 | view.addSubview(todoListView) 45 | 46 | updateView(with: screen) 47 | } 48 | 49 | override func viewDidLayoutSubviews() { 50 | super.viewDidLayoutSubviews() 51 | 52 | todoListView.frame = view.bounds.inset(by: view.safeAreaInsets) 53 | } 54 | 55 | override func screenDidChange(from previousScreen: TodoListScreen, previousEnvironment: ViewEnvironment) { 56 | super.screenDidChange(from: previousScreen, previousEnvironment: previousEnvironment) 57 | 58 | guard isViewLoaded else { return } 59 | 60 | updateView(with: screen) 61 | } 62 | 63 | private func updateView(with screen: TodoListScreen) { 64 | // Update the todoList on the view with what the screen provided: 65 | todoListView.todoList = screen.todoTitles 66 | todoListView.onTodoSelected = screen.onTodoSelected 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Samples/Tutorial/Frameworks/Tutorial5Complete/Sources/Todo/Model/TodoModel.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | struct TodoModel: Equatable { 18 | var title: String 19 | var note: String 20 | } 21 | -------------------------------------------------------------------------------- /Samples/Tutorial/Frameworks/Tutorial5Complete/Sources/TutorialHostingViewController.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import BackStackContainer 18 | import UIKit 19 | import Workflow 20 | import WorkflowUI 21 | 22 | public final class TutorialHostingViewController: UIViewController { 23 | let containerViewController: UIViewController 24 | 25 | public init() { 26 | // Create a `WorkflowHostingController` with the `RootWorkflow` as the root workflow 27 | self.containerViewController = WorkflowHostingController(workflow: RootWorkflow()) 28 | 29 | super.init(nibName: nil, bundle: nil) 30 | } 31 | 32 | @available(*, unavailable) 33 | required init?(coder aDecoder: NSCoder) { 34 | fatalError("init(coder:) has not been implemented") 35 | } 36 | 37 | override public func viewDidLoad() { 38 | super.viewDidLoad() 39 | 40 | view.backgroundColor = .white 41 | 42 | addChild(containerViewController) 43 | view.addSubview(containerViewController.view) 44 | containerViewController.didMove(toParent: self) 45 | } 46 | 47 | override public func viewDidLayoutSubviews() { 48 | super.viewDidLayoutSubviews() 49 | 50 | containerViewController.view.frame = view.bounds 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Samples/Tutorial/Frameworks/Tutorial5Complete/Sources/Welcome/WelcomeScreen.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import TutorialViews 18 | import Workflow 19 | import WorkflowUI 20 | 21 | struct WelcomeScreen: Screen { 22 | /// The current name that has been entered. 23 | var name: String 24 | /// Callback when the name changes in the UI. 25 | var onNameChanged: (String) -> Void 26 | /// Callback when the login button is tapped. 27 | var onLoginTapped: () -> Void 28 | 29 | func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { 30 | WelcomeViewController.description(for: self, environment: environment) 31 | } 32 | } 33 | 34 | final class WelcomeViewController: ScreenViewController { 35 | private var welcomeView: WelcomeView! 36 | 37 | required init(screen: WelcomeScreen, environment: ViewEnvironment) { 38 | super.init(screen: screen, environment: environment) 39 | } 40 | 41 | override func viewDidLoad() { 42 | super.viewDidLoad() 43 | 44 | welcomeView = WelcomeView(frame: view.bounds) 45 | updateView(with: screen) 46 | 47 | view.addSubview(welcomeView) 48 | } 49 | 50 | override func viewDidLayoutSubviews() { 51 | super.viewDidLayoutSubviews() 52 | 53 | welcomeView.frame = view.bounds.inset(by: view.safeAreaInsets) 54 | } 55 | 56 | override func screenDidChange(from previousScreen: WelcomeScreen, previousEnvironment: ViewEnvironment) { 57 | super.screenDidChange(from: previousScreen, previousEnvironment: previousEnvironment) 58 | 59 | guard isViewLoaded else { return } 60 | 61 | updateView(with: screen) 62 | } 63 | 64 | private func updateView(with screen: WelcomeScreen) { 65 | welcomeView.name = screen.name 66 | welcomeView.onNameChanged = screen.onNameChanged 67 | welcomeView.onLoginTapped = screen.onLoginTapped 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Samples/Tutorial/Frameworks/Tutorial5Complete/Tests/TodoListWorkflowTests.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import WorkflowTesting 18 | import XCTest 19 | @testable import Tutorial5Complete 20 | 21 | class TodoListWorkflowTests: XCTestCase { 22 | func testActions() throws { 23 | TodoListWorkflow.Action 24 | .tester(withState: TodoListWorkflow.State()) 25 | .send(action: .onBack) 26 | .verifyOutput { output in 27 | // The `.onBack` action should emit an output of `.back`. 28 | switch output { 29 | case .back: 30 | break // Expected 31 | default: 32 | XCTFail("Expected an output of `.back`") 33 | } 34 | } 35 | .send(action: .selectTodo(index: 7)) 36 | .verifyOutput { output in 37 | // The `.selectTodo` action should emit a `.selectTodo` output. 38 | switch output { 39 | case .selectTodo(index: let index): 40 | XCTAssertEqual(7, index) 41 | default: 42 | XCTFail("Expected an output of `.selectTodo`") 43 | } 44 | } 45 | .send(action: .new) 46 | .verifyOutput { output in 47 | // The `.new` action should emit a `.newTodo` output. 48 | switch output { 49 | case .newTodo: 50 | break // Expected 51 | default: 52 | XCTFail("Expected an output of `.newTodo`") 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Samples/Tutorial/Frameworks/Tutorial5Complete/Tests/TutorialTests.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import XCTest 18 | 19 | final class TutorialTests: XCTestCase { 20 | func testPlaceholder() { 21 | XCTAssertEqual(1, 1, "Placeholder test") 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Samples/Tutorial/Frameworks/TutorialBase/Sources/Todo/Edit/TodoEditSampleViewController.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import TutorialViews 18 | import UIKit 19 | 20 | final class TodoEditSampleViewController: UIViewController { 21 | let todoEditView: TodoEditView 22 | 23 | init() { 24 | self.todoEditView = TodoEditView(frame: .zero) 25 | 26 | super.init(nibName: nil, bundle: nil) 27 | } 28 | 29 | @available(*, unavailable) 30 | required init?(coder aDecoder: NSCoder) { 31 | fatalError("init(coder:) has not been implemented") 32 | } 33 | 34 | // MARK: UIViewController 35 | 36 | override func viewDidLoad() { 37 | super.viewDidLoad() 38 | 39 | view.addSubview(todoEditView) 40 | } 41 | 42 | override func viewDidLayoutSubviews() { 43 | super.viewDidLayoutSubviews() 44 | 45 | todoEditView.frame = view.bounds.inset(by: view.safeAreaInsets) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Samples/Tutorial/Frameworks/TutorialBase/Sources/Todo/List/TodoListSampleViewController.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import TutorialViews 18 | import UIKit 19 | 20 | final class TodoListSampleViewController: UIViewController { 21 | let todoListView: TodoListView 22 | 23 | init() { 24 | self.todoListView = TodoListView(frame: .zero) 25 | 26 | super.init(nibName: nil, bundle: nil) 27 | } 28 | 29 | @available(*, unavailable) 30 | required init?(coder aDecoder: NSCoder) { 31 | fatalError("init(coder:) has not been implemented") 32 | } 33 | 34 | // MARK: UIViewController 35 | 36 | override func viewDidLoad() { 37 | super.viewDidLoad() 38 | 39 | view.addSubview(todoListView) 40 | } 41 | 42 | override func viewDidLayoutSubviews() { 43 | super.viewDidLayoutSubviews() 44 | 45 | todoListView.frame = view.bounds.inset(by: view.safeAreaInsets) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Samples/Tutorial/Frameworks/TutorialBase/Sources/Todo/Model/TodoModel.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | struct TodoModel: Equatable { 18 | var title: String 19 | var note: String 20 | } 21 | -------------------------------------------------------------------------------- /Samples/Tutorial/Frameworks/TutorialBase/Sources/TutorialHostingViewController.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import UIKit 18 | import Workflow 19 | import WorkflowUI 20 | 21 | public final class TutorialHostingViewController: UIViewController { 22 | let containerViewController: UIViewController 23 | 24 | public init() { 25 | // Show one of the sample view controllers, to demonstrate the provided views: 26 | self.containerViewController = WelcomeSampleViewController() 27 | 28 | super.init(nibName: nil, bundle: nil) 29 | } 30 | 31 | @available(*, unavailable) 32 | required init?(coder aDecoder: NSCoder) { 33 | fatalError("init(coder:) has not been implemented") 34 | } 35 | 36 | override public func viewDidLoad() { 37 | super.viewDidLoad() 38 | 39 | view.backgroundColor = .white 40 | 41 | addChild(containerViewController) 42 | view.addSubview(containerViewController.view) 43 | containerViewController.didMove(toParent: self) 44 | } 45 | 46 | override public func viewDidLayoutSubviews() { 47 | super.viewDidLayoutSubviews() 48 | 49 | containerViewController.view.frame = view.bounds 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Samples/Tutorial/Frameworks/TutorialBase/Sources/Welcome/WelcomeSampleViewController.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import TutorialViews 18 | import UIKit 19 | 20 | final class WelcomeSampleViewController: UIViewController { 21 | let welcomeView: WelcomeView 22 | 23 | init() { 24 | self.welcomeView = WelcomeView(frame: .zero) 25 | super.init(nibName: nil, bundle: nil) 26 | } 27 | 28 | @available(*, unavailable) 29 | required init?(coder aDecoder: NSCoder) { 30 | fatalError("init(coder:) has not been implemented") 31 | } 32 | 33 | override func viewDidLoad() { 34 | super.viewDidLoad() 35 | 36 | view.addSubview(welcomeView) 37 | } 38 | 39 | override func viewDidLayoutSubviews() { 40 | super.viewDidLayoutSubviews() 41 | 42 | welcomeView.frame = view.bounds 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Samples/Tutorial/Frameworks/TutorialBase/Tests/TutorialTests.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import XCTest 18 | 19 | final class TutorialTests: XCTestCase { 20 | func testPlaceholder() { 21 | XCTAssertEqual(1, 1, "Placeholder test") 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Samples/Tutorial/README.md: -------------------------------------------------------------------------------- 1 | # Tutorial 2 | 3 | ## Overview 4 | 5 | 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. 6 | 7 | ## Layout 8 | 9 | The project has both a starting point, as well as an example of the completed tutorial. 10 | 11 | Nearly all of the code is in the `Frameworks` directory. 12 | 13 | To help with the setup, we have created a few helpers: 14 | - `TutorialViews`: A set of 3 views for the 3 screens we will be building, `Welcome`, `TodoList`, and `TodoEdit`. 15 | - `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. 16 | - Additionally, there is a `TutorialHostingViewController` that the AppDelegate sets as the root view controller. This will be our launching point for all of our workflows. 17 | - `TutorialFinal`: This is an example of the completed tutorial - could be used as a reference if you get stuck. 18 | 19 | ## Getting started 20 | 21 | The tutorial uses [Tuist](https://tuist.io/) for project configuration. Follow the main README instructions for getting set up with Tuist first, and then run: 22 | 23 | ``` 24 | $ cd Samples/Tutorial 25 | $ tuist install 26 | Resolving and fetching plugins. 27 | Plugins resolved and fetched successfully. 28 | Resolving and fetching dependencies. 29 | ... 30 | $ tuist generate 31 | Loading and constructing the graph 32 | It might take a while if the cache is empty 33 | Using cache binaries for the following targets: 34 | Generating workspace Tutorial.xcworkspace 35 | Generating project Workflow 36 | Generating project Tutorial 37 | Generating project swift-case-paths 38 | Generating project WorkflowDevelopment 39 | Generating project xctest-dynamic-overlay 40 | Generating project swift-identified-collections 41 | Generating project ReactiveSwift 42 | Generating project swift-collections 43 | Generating project swift-perception 44 | Generating project iOSSnapshotTestCase 45 | Generating project RxSwift 46 | Generating project swift-syntax 47 | Project generated. 48 | ``` 49 | 50 | The `Tutorial.xcworkspace` workspace will open in Xcode automatically. 51 | 52 | # Tutorial Steps 53 | 54 | - [Tutorial 1](Tutorial1.md) - Single view backed by a workflow 55 | - [Tutorial 2](Tutorial2.md) - Multiple views and navigation 56 | - [Tutorial 3](Tutorial3.md) - State throughout a tree of workflows 57 | - [Tutorial 4](Tutorial4.md) - Refactoring 58 | - [Tutorial 5](Tutorial5.md) - Testing 59 | -------------------------------------------------------------------------------- /Samples/Tutorial/images/empty-todolist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/square/workflow-swift/53fc18d87e1785891b64f2dda782bae47f595399/Samples/Tutorial/images/empty-todolist.png -------------------------------------------------------------------------------- /Samples/Tutorial/images/full-edit-flow.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/square/workflow-swift/53fc18d87e1785891b64f2dda782bae47f595399/Samples/Tutorial/images/full-edit-flow.gif -------------------------------------------------------------------------------- /Samples/Tutorial/images/missing-map-output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/square/workflow-swift/53fc18d87e1785891b64f2dda782bae47f595399/Samples/Tutorial/images/missing-map-output.png -------------------------------------------------------------------------------- /Samples/Tutorial/images/new-screen-todolist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/square/workflow-swift/53fc18d87e1785891b64f2dda782bae47f595399/Samples/Tutorial/images/new-screen-todolist.png -------------------------------------------------------------------------------- /Samples/Tutorial/images/new-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/square/workflow-swift/53fc18d87e1785891b64f2dda782bae47f595399/Samples/Tutorial/images/new-screen.png -------------------------------------------------------------------------------- /Samples/Tutorial/images/new-todolist-workflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/square/workflow-swift/53fc18d87e1785891b64f2dda782bae47f595399/Samples/Tutorial/images/new-todolist-workflow.png -------------------------------------------------------------------------------- /Samples/Tutorial/images/new-workflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/square/workflow-swift/53fc18d87e1785891b64f2dda782bae47f595399/Samples/Tutorial/images/new-workflow.png -------------------------------------------------------------------------------- /Samples/Tutorial/images/tut2-todolist-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/square/workflow-swift/53fc18d87e1785891b64f2dda782bae47f595399/Samples/Tutorial/images/tut2-todolist-example.png -------------------------------------------------------------------------------- /Samples/Tutorial/images/welcome-to-todolist.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/square/workflow-swift/53fc18d87e1785891b64f2dda782bae47f595399/Samples/Tutorial/images/welcome-to-todolist.gif -------------------------------------------------------------------------------- /Samples/Tutorial/images/welcome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/square/workflow-swift/53fc18d87e1785891b64f2dda782bae47f595399/Samples/Tutorial/images/welcome.png -------------------------------------------------------------------------------- /Samples/Tutorial/images/workflow-file-location.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/square/workflow-swift/53fc18d87e1785891b64f2dda782bae47f595399/Samples/Tutorial/images/workflow-file-location.png -------------------------------------------------------------------------------- /Samples/Tutorial/images/workflow-name.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/square/workflow-swift/53fc18d87e1785891b64f2dda782bae47f595399/Samples/Tutorial/images/workflow-name.png -------------------------------------------------------------------------------- /Samples/WorkflowCombineSampleApp/WorkflowCombineSampleApp/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // WorkflowCombineSampleApp 4 | // 5 | // Created by Soo Rin Park on 10/28/21. 6 | // 7 | 8 | import UIKit 9 | import WorkflowUI 10 | 11 | @main 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | var window: UIWindow? 14 | 15 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 16 | window = UIWindow(frame: UIScreen.main.bounds) 17 | window?.rootViewController = WorkflowHostingController(workflow: DemoWorkflow()) 18 | window?.makeKeyAndVisible() 19 | 20 | return true 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Samples/WorkflowCombineSampleApp/WorkflowCombineSampleApp/DemoViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DemoViewController.swift 3 | // WorkflowCombineSampleApp 4 | // 5 | // Created by Soo Rin Park on 10/28/21. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | import WorkflowUI 11 | 12 | class DemoViewController: ScreenViewController { 13 | private let label = UILabel() 14 | 15 | override func viewDidLoad() { 16 | super.viewDidLoad() 17 | 18 | label.translatesAutoresizingMaskIntoConstraints = false 19 | view.addSubview(label) 20 | label.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true 21 | label.topAnchor.constraint(equalTo: view.topAnchor).isActive = true 22 | label.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true 23 | label.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true 24 | } 25 | 26 | override func screenDidChange(from previousScreen: DemoScreen, previousEnvironment: ViewEnvironment) { 27 | super.screenDidChange(from: previousScreen, previousEnvironment: previousEnvironment) 28 | 29 | label.text = screen.date 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Samples/WorkflowCombineSampleApp/WorkflowCombineSampleApp/DemoWorker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DemoWorker.swift 3 | // WorkflowCombineSampleApp 4 | // 5 | // Created by Soo Rin Park on 10/28/21. 6 | // 7 | 8 | import Combine 9 | import Foundation 10 | import Workflow 11 | import WorkflowCombine 12 | 13 | // MARK: Workers 14 | 15 | extension DemoWorkflow { 16 | struct DemoWorker: WorkflowCombine.Worker { 17 | typealias Output = Action 18 | 19 | // This publisher publishes the current date on a timer that fires every second 20 | func run() -> AnyPublisher { 21 | Timer.publish(every: 2, on: .main, in: .common) 22 | .autoconnect() 23 | .map { Action(publishedDate: $0) } 24 | .eraseToAnyPublisher() 25 | } 26 | 27 | func isEquivalent(to otherWorker: DemoWorkflow.DemoWorker) -> Bool { true } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Samples/WorkflowCombineSampleApp/WorkflowCombineSampleApp/DemoWorkflow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DemoWorkflow.swift 3 | // WorkflowCombineSampleApp 4 | // 5 | // Created by Soo Rin Park on 10/28/21. 6 | // 7 | 8 | import Foundation 9 | import Workflow 10 | import WorkflowUI 11 | 12 | // MARK: Input and Output 13 | 14 | let dateFormatter = DateFormatter() 15 | 16 | struct DemoWorkflow: Workflow { 17 | typealias Output = Never 18 | } 19 | 20 | // MARK: State and Initialization 21 | 22 | extension DemoWorkflow { 23 | struct State: Equatable { 24 | var date: Date 25 | } 26 | 27 | func makeInitialState() -> DemoWorkflow.State { State(date: Date()) } 28 | 29 | func workflowDidChange(from previousWorkflow: DemoWorkflow, state: inout State) {} 30 | } 31 | 32 | // MARK: Actions 33 | 34 | extension DemoWorkflow { 35 | struct Action: WorkflowAction { 36 | typealias WorkflowType = DemoWorkflow 37 | 38 | let publishedDate: Date 39 | 40 | func apply(toState state: inout DemoWorkflow.State, context: ApplyContext) -> DemoWorkflow.Output? { 41 | state.date = publishedDate 42 | return nil 43 | } 44 | } 45 | } 46 | 47 | // MARK: Rendering 48 | 49 | extension DemoWorkflow { 50 | typealias Rendering = DemoScreen 51 | 52 | func render(state: DemoWorkflow.State, context: RenderContext) -> Rendering { 53 | // Combine-based worker example 54 | DemoWorker() 55 | .rendered(in: context) 56 | 57 | // Directly consume a Publisher 58 | Timer.publish(every: 2, on: .main, in: .common) 59 | .autoconnect() 60 | .delay(for: 1.0, scheduler: DispatchQueue.main) 61 | .asAnyWorkflow() 62 | .onOutput { state, output in 63 | state.date = Date() 64 | return nil 65 | } 66 | .rendered(in: context) 67 | 68 | dateFormatter.dateStyle = .long 69 | dateFormatter.timeStyle = .long 70 | let formattedDate = dateFormatter.string(from: state.date) 71 | let rendering = Rendering(date: formattedDate) 72 | 73 | return rendering 74 | } 75 | } 76 | 77 | struct DemoScreen: Screen { 78 | let date: String 79 | 80 | func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { 81 | DemoViewController.description(for: self, environment: environment) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Samples/WorkflowCombineSampleApp/WorkflowCombineSampleAppUnitTests/DemoWorkflowTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DemoWorkflowTests.swift 3 | // WorkflowCombineSampleAppUnitTests 4 | // 5 | // Created by Soo Rin Park on 11/1/21. 6 | // 7 | 8 | import Combine 9 | import Workflow 10 | import WorkflowTesting 11 | import XCTest 12 | @testable import WorkflowCombineSampleApp 13 | 14 | class DemoWorkflowTests: XCTestCase { 15 | func test_demoWorkflow_publishesNewDate() { 16 | let expectedDate = Date(timeIntervalSince1970: 0) 17 | 18 | DemoWorkflow 19 | .Action 20 | .tester(withState: .init(date: Date())) // the initial date itself does not matter 21 | .send(action: .init(publishedDate: expectedDate)) 22 | .assert(state: .init(date: expectedDate)) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Samples/Workspace.swift: -------------------------------------------------------------------------------- 1 | import ProjectDescription 2 | import ProjectDescriptionHelpers 3 | 4 | let workspace = Workspace( 5 | name: "WorkflowDevelopment", 6 | projects: [".", "Tutorial"], 7 | schemes: [ 8 | // Generate a scheme for each target in Package.swift for convenience 9 | .workflow("Workflow"), 10 | .workflow("WorkflowTesting"), 11 | .workflow("WorkflowUI"), 12 | .workflow("WorkflowSwiftUI"), 13 | .workflow("WorkflowSwiftUIMacros"), 14 | .workflow("WorkflowReactiveSwift"), 15 | .workflow("WorkflowReactiveSwiftTesting"), 16 | .workflow("WorkflowRxSwift"), 17 | .workflow("WorkflowRxSwiftTesting"), 18 | .workflow("WorkflowCombine"), 19 | .workflow("WorkflowCombineTesting"), 20 | .workflow("WorkflowConcurrency"), 21 | .workflow("WorkflowConcurrencyTesting"), 22 | .workflow("ViewEnvironment"), 23 | .workflow("ViewEnvironmentUI"), 24 | .scheme( 25 | name: "Documentation", 26 | buildAction: .buildAction( 27 | targets: [ 28 | .project(path: "..", target: "ViewEnvironment"), 29 | .project(path: "..", target: "ViewEnvironmentUI"), 30 | .project(path: "..", target: "Workflow"), 31 | .project(path: "..", target: "WorkflowSwiftUI"), 32 | .project(path: "..", target: "WorkflowTesting"), 33 | .project(path: "..", target: "WorkflowUI"), 34 | ] 35 | ) 36 | ), 37 | ] 38 | ) 39 | 40 | extension Scheme { 41 | public static func workflow(_ target: String) -> Self { 42 | .scheme( 43 | name: target, 44 | buildAction: .buildAction(targets: [.project(path: "..", target: target)]) 45 | ) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Scripts/generate_docs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | BUILD_PATH=docs_build 6 | MERGED_PATH=generated_docs 7 | REPO_NAME=workflow-swift 8 | 9 | xcodebuild docbuild \ 10 | -scheme Documentation \ 11 | -derivedDataPath "$BUILD_PATH" \ 12 | -workspace Samples/WorkflowDevelopment.xcworkspace \ 13 | -destination generic/platform=iOS \ 14 | DOCC_HOSTING_BASE_PATH="$REPO_NAME" \ 15 | | xcpretty 16 | 17 | find_archive() { 18 | find "$BUILD_PATH" -type d -name "$1.doccarchive" -print -quit 19 | } 20 | 21 | xcrun docc merge \ 22 | $(find_archive ViewEnvironment) \ 23 | $(find_archive ViewEnvironmentUI) \ 24 | $(find_archive Workflow) \ 25 | $(find_archive WorkflowSwiftUI) \ 26 | $(find_archive WorkflowTesting) \ 27 | $(find_archive WorkflowUI) \ 28 | --output-path "$MERGED_PATH" \ 29 | --synthesized-landing-page-name "$REPO_NAME" 30 | -------------------------------------------------------------------------------- /TestingSupport/AppHost/Sources/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | @main 4 | class AppDelegate: UIResponder, UIApplicationDelegate { 5 | var window: UIWindow? 6 | 7 | func application( 8 | _ application: UIApplication, 9 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil 10 | ) -> Bool { 11 | window = UIWindow(frame: UIScreen.main.bounds) 12 | window?.rootViewController = UIViewController() 13 | 14 | window?.makeKeyAndVisible() 15 | 16 | return true 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Tooling/Templates/Screen (View Controller).xctemplate/TemplateIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/square/workflow-swift/53fc18d87e1785891b64f2dda782bae47f595399/Tooling/Templates/Screen (View Controller).xctemplate/TemplateIcon.png -------------------------------------------------------------------------------- /Tooling/Templates/Screen (View Controller).xctemplate/TemplateIcon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/square/workflow-swift/53fc18d87e1785891b64f2dda782bae47f595399/Tooling/Templates/Screen (View Controller).xctemplate/TemplateIcon@2x.png -------------------------------------------------------------------------------- /Tooling/Templates/Screen (View Controller).xctemplate/TemplateInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Kind 6 | Xcode.IDEKit.TextSubstitutionFileTemplateKind 7 | Platforms 8 | 9 | com.apple.platform.iphoneos 10 | 11 | Options 12 | 13 | 14 | Identifier 15 | productName 16 | Required 17 | 18 | Name 19 | Screen Name: 20 | Description 21 | The name of the screen to create 22 | Type 23 | text 24 | Default 25 | HelloWorld 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /Tooling/Templates/Screen (View Controller).xctemplate/___FILEBASENAME___Screen.swift: -------------------------------------------------------------------------------- 1 | // ___FILEHEADER___ 2 | 3 | import Workflow 4 | import WorkflowUI 5 | 6 | struct ___VARIABLE_productName___Screen: Screen { 7 | // This should contain all data to display in the UI 8 | 9 | // It should also contain callbacks for any UI events, for example: 10 | // var onButtonTapped: () -> Void 11 | 12 | func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { 13 | return ___VARIABLE_productName___ViewController.description(for: self, environment: environment) 14 | } 15 | } 16 | 17 | final class ___VARIABLE_productName___ViewController: ScreenViewController<___VARIABLE_productName___Screen> { 18 | required init(screen: ___VARIABLE_productName___Screen, environment: ViewEnvironment) { 19 | super.init(screen: screen, environment: environment) 20 | } 21 | 22 | override func screenDidChange(from previousScreen: ___VARIABLE_productName___Screen, previousEnvironment: ViewEnvironment) { 23 | super.screenDidChange(from: previousScreen, previousEnvironment: previousEnvironment) 24 | 25 | // Update UI 26 | } 27 | } 28 | 29 | -------------------------------------------------------------------------------- /Tooling/Templates/Workflow (Verbose).xctemplate/Default/___FILEBASENAME___Workflow.swift: -------------------------------------------------------------------------------- 1 | // ___FILEHEADER___ 2 | 3 | import Workflow 4 | import WorkflowUI 5 | 6 | // MARK: Input and Output 7 | 8 | struct ___VARIABLE_productName___Workflow: Workflow { 9 | enum Output {} 10 | } 11 | 12 | // MARK: State and Initialization 13 | 14 | extension ___VARIABLE_productName___Workflow { 15 | struct State {} 16 | 17 | func makeInitialState() -> ___VARIABLE_productName___Workflow.State { 18 | return State() 19 | } 20 | } 21 | 22 | // MARK: Actions 23 | 24 | extension ___VARIABLE_productName___Workflow { 25 | enum Action: WorkflowAction { 26 | typealias WorkflowType = ___VARIABLE_productName___Workflow 27 | 28 | func applytoState state: inout ___VARIABLE_productName___Workflow.State, context: ApplyContext -> ___VARIABLE_productName___Workflow.Output? { 29 | switch self { 30 | // Update state and produce an optional output based on which action was received. 31 | } 32 | } 33 | } 34 | } 35 | 36 | // MARK: Rendering 37 | 38 | extension ___VARIABLE_productName___Workflow { 39 | // TODO: Change this to your actual rendering type 40 | typealias Rendering = String 41 | 42 | func render(state: ___VARIABLE_productName___Workflow.State, context: RenderContext<___VARIABLE_productName___Workflow>) -> Rendering { 43 | #warning("Don't forget your render implementation and to return the correct rendering type!") 44 | return "This is likely not the rendering that you want to return" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Tooling/Templates/Workflow (Verbose).xctemplate/TemplateIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/square/workflow-swift/53fc18d87e1785891b64f2dda782bae47f595399/Tooling/Templates/Workflow (Verbose).xctemplate/TemplateIcon.png -------------------------------------------------------------------------------- /Tooling/Templates/Workflow (Verbose).xctemplate/TemplateIcon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/square/workflow-swift/53fc18d87e1785891b64f2dda782bae47f595399/Tooling/Templates/Workflow (Verbose).xctemplate/TemplateIcon@2x.png -------------------------------------------------------------------------------- /Tooling/Templates/Workflow (Verbose).xctemplate/TemplateInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Kind 6 | Xcode.IDEKit.TextSubstitutionFileTemplateKind 7 | Platforms 8 | 9 | com.apple.platform.iphoneos 10 | 11 | Options 12 | 13 | 14 | Identifier 15 | productName 16 | Required 17 | 18 | Name 19 | Name: 20 | Description 21 | The name of the workflow to create. "Workflow" will be appended to the name. 22 | Type 23 | text 24 | Default 25 | HelloWorld 26 | 27 | 28 | Identifier 29 | generateWorker 30 | Required 31 | 32 | Name 33 | Also create a Worker 34 | Description 35 | Workers define a unit of asynchronous work 36 | Type 37 | checkbox 38 | Default 39 | false 40 | 41 | 42 | Identifier 43 | streamType 44 | Required 45 | 46 | Name 47 | Uses: 48 | Description 49 | Whether to use RxSwift or ReactiveSwift 50 | Type 51 | popup 52 | Default 53 | ReactiveSwift 54 | RequiredOptions 55 | 56 | generateWorker 57 | 58 | true 59 | 60 | 61 | Values 62 | 63 | RxSwift 64 | ReactiveSwift 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /Tooling/Templates/Workflow (Verbose).xctemplate/generateWorkerReactiveSwift/___FILEBASENAME___Worker.swift: -------------------------------------------------------------------------------- 1 | // ___FILEHEADER___ 2 | 3 | import ReactiveSwift 4 | import Workflow 5 | import WorkflowReactiveSwift 6 | import WorkflowUI 7 | 8 | // MARK: Workers 9 | 10 | extension ___VARIABLE_productName___Workflow { 11 | struct ___VARIABLE_productName___Worker: Worker { 12 | enum Output {} 13 | 14 | func run() -> SignalProducer { 15 | fatalError() 16 | } 17 | 18 | func isEquivalent(to otherWorker: ___VARIABLE_productName___Worker) -> Bool { 19 | return true 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Tooling/Templates/Workflow (Verbose).xctemplate/generateWorkerReactiveSwift/___FILEBASENAME___Workflow.swift: -------------------------------------------------------------------------------- 1 | // ___FILEHEADER___ 2 | 3 | import Workflow 4 | import WorkflowUI 5 | 6 | // MARK: Input and Output 7 | 8 | struct ___VARIABLE_productName___Workflow: Workflow { 9 | enum Output {} 10 | } 11 | 12 | // MARK: State and Initialization 13 | 14 | extension ___VARIABLE_productName___Workflow { 15 | struct State {} 16 | 17 | func makeInitialState() -> ___VARIABLE_productName___Workflow.State { 18 | return State() 19 | } 20 | } 21 | 22 | // MARK: Actions 23 | 24 | extension ___VARIABLE_productName___Workflow { 25 | enum Action: WorkflowAction { 26 | typealias WorkflowType = ___VARIABLE_productName___Workflow 27 | 28 | func apply(toState state: inout ___VARIABLE_productName___Workflow.State, context: ApplyContext) -> ___VARIABLE_productName___Workflow.Output? { 29 | switch self { 30 | // Update state and produce an optional output based on which action was received. 31 | } 32 | } 33 | } 34 | } 35 | 36 | // MARK: Rendering 37 | 38 | extension ___VARIABLE_productName___Workflow { 39 | // TODO: Change this to your actual rendering type 40 | typealias Rendering = String 41 | 42 | func render(state: ___VARIABLE_productName___Workflow.State, context: RenderContext<___VARIABLE_productName___Workflow>) -> Rendering { 43 | #warning("Don't forget your render implementation and to return the correct rendering type!") 44 | return "This is likely not the rendering that you want to return" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Tooling/Templates/Workflow (Verbose).xctemplate/generateWorkerRxSwift/___FILEBASENAME___Worker.swift: -------------------------------------------------------------------------------- 1 | // ___FILEHEADER___ 2 | 3 | import RxSwift 4 | import Workflow 5 | import WorkflowRxSwift 6 | import WorkflowUI 7 | 8 | // MARK: Workers 9 | 10 | extension ___VARIABLE_productName___Workflow { 11 | struct ___VARIABLE_productName___Worker: Worker { 12 | enum Output {} 13 | 14 | func run() -> Observable { 15 | fatalError() 16 | } 17 | 18 | func isEquivalent(to otherWorker: ___VARIABLE_productName___Worker) -> Bool { 19 | return true 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Tooling/Templates/Workflow (Verbose).xctemplate/generateWorkerRxSwift/___FILEBASENAME___Workflow.swift: -------------------------------------------------------------------------------- 1 | // ___FILEHEADER___ 2 | 3 | import Workflow 4 | import WorkflowUI 5 | 6 | // MARK: Input and Output 7 | 8 | struct ___VARIABLE_productName___Workflow: Workflow { 9 | enum Output {} 10 | } 11 | 12 | // MARK: State and Initialization 13 | 14 | extension ___VARIABLE_productName___Workflow { 15 | struct State {} 16 | 17 | func makeInitialState() -> ___VARIABLE_productName___Workflow.State { 18 | return State() 19 | } 20 | } 21 | 22 | // MARK: Actions 23 | 24 | extension ___VARIABLE_productName___Workflow { 25 | enum Action: WorkflowAction { 26 | typealias WorkflowType = ___VARIABLE_productName___Workflow 27 | 28 | func apply(toState state: inout ___VARIABLE_productName___Workflow.State, context: ApplyContext) -> ___VARIABLE_productName___Workflow.Output? { 29 | switch self { 30 | // Update state and produce an optional output based on which action was received. 31 | } 32 | } 33 | } 34 | } 35 | 36 | // MARK: Rendering 37 | 38 | extension ___VARIABLE_productName___Workflow { 39 | // TODO: Change this to your actual rendering type 40 | typealias Rendering = String 41 | 42 | func render(state: ___VARIABLE_productName___Workflow.State, context: RenderContext<___VARIABLE_productName___Workflow>) -> Rendering { 43 | #warning("Don't forget your render implementation and to return the correct rendering type!") 44 | return "This is likely not the rendering that you want to return" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Tooling/Templates/install-xcode-templates.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # Configuration 4 | XCODE_TEMPLATE_DIR=$HOME'/Library/Developer/Xcode/Templates/File Templates/Workflow' 5 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 6 | 7 | # Copy workflow file templates into the local workflow template directory 8 | xcodeTemplate () { 9 | echo "Copying workflow Xcode file templates..." 10 | 11 | mkdir -p "$XCODE_TEMPLATE_DIR" 12 | 13 | cp -R $SCRIPT_DIR/*.xctemplate "$XCODE_TEMPLATE_DIR" 14 | } 15 | 16 | xcodeTemplate 17 | 18 | echo "Success!" 19 | echo "Workflow templates have been installed. Remember to restart Xcode!" 20 | -------------------------------------------------------------------------------- /ViewEnvironment/Sources/EnvironmentValues+ViewEnvironment.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #if canImport(SwiftUI) 18 | 19 | import SwiftUI 20 | 21 | extension EnvironmentValues { 22 | public var viewEnvironment: ViewEnvironment { 23 | get { self[ViewEnvironmentKey.self] } 24 | set { self[ViewEnvironmentKey.self] = newValue } 25 | } 26 | 27 | private struct ViewEnvironmentKey: EnvironmentKey { 28 | static let defaultValue: ViewEnvironment = .empty 29 | } 30 | } 31 | 32 | extension Environment where Value == ViewEnvironment { 33 | @available( 34 | *, 35 | deprecated, 36 | message: 37 | """ 38 | Please do not create an `@Environment` property that references the top-level `viewEnvironment`: \ 39 | it will break SwiftUI's automatic invalidation when any part of the `ViewEnvironment` changes. \ 40 | Instead, reference your relevant sub-property, eg `@Environment(\\.viewEnvironment.myProperty)`. 41 | """ 42 | ) 43 | @inlinable public init(_ keyPath: KeyPath) { 44 | fatalError( 45 | """ 46 | Please do not create an `@Environment` property that references the top-level `viewEnvironment`: \ 47 | it will break SwiftUI's automatic invalidation when any part of the `ViewEnvironment` changes. \ 48 | Instead, reference your relevant sub-property, eg `@Environment(\\.viewEnvironment.myProperty)`. 49 | """ 50 | ) 51 | } 52 | } 53 | 54 | #endif 55 | -------------------------------------------------------------------------------- /ViewEnvironment/Sources/ViewEnvironmentKey.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /// A key into the ViewEnvironment. 18 | /// 19 | /// Environment keys are associated with a specific type of value (`Value`) and 20 | /// must declare a default value. 21 | /// 22 | /// Typically the key conforming to `ViewEnvironmentKey` will be private, and 23 | /// you are encouraged to provide a convenience accessor on `ViewEnvironment` 24 | /// as in the following example: 25 | /// 26 | /// ``` 27 | /// private enum ThemeKey: ViewEnvironmentKey { 28 | /// typealias Value = Theme 29 | /// var defaultValue: Theme 30 | /// } 31 | /// 32 | /// extension ViewEnvironment { 33 | /// public var theme: Theme { 34 | /// get { self[ThemeKey.self] } 35 | /// set { self[ThemeKey.self] = newValue } 36 | /// } 37 | /// } 38 | /// ``` 39 | public protocol ViewEnvironmentKey { 40 | associatedtype Value 41 | 42 | static var defaultValue: Value { get } 43 | } 44 | -------------------------------------------------------------------------------- /ViewEnvironmentUI/README.md: -------------------------------------------------------------------------------- 1 | # ViewEnvironmentUI 2 | 3 | `ViewEnvironmentUI` provides a means to propagate a `ViewEnvironment` through a hierarchy of object nodes. 4 | 5 | Support for propagation of `ViewEnvironment` through `UIViewController`s and `UIView`s is provided by this framework. 6 | 7 | -------------------------------------------------------------------------------- /ViewEnvironmentUI/Sources/UIView+ViewEnvironmentPropagating.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #if canImport(UIKit) 18 | 19 | import UIKit 20 | import ViewEnvironment 21 | 22 | extension UIView: ViewEnvironmentPropagating { 23 | @_spi(ViewEnvironmentWiring) 24 | public var defaultEnvironmentAncestor: ViewEnvironmentPropagating? { superview } 25 | 26 | @_spi(ViewEnvironmentWiring) 27 | public var defaultEnvironmentDescendants: [ViewEnvironmentPropagating] { subviews } 28 | 29 | @_spi(ViewEnvironmentWiring) 30 | public func setNeedsApplyEnvironment() { 31 | setNeedsLayout() 32 | } 33 | } 34 | 35 | #endif 36 | -------------------------------------------------------------------------------- /ViewEnvironmentUI/Sources/UIViewController+ViewEnvironmentPropagating.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #if canImport(UIKit) 18 | 19 | import UIKit 20 | import ViewEnvironment 21 | 22 | extension UIViewController: ViewEnvironmentPropagating { 23 | @_spi(ViewEnvironmentWiring) 24 | public var defaultEnvironmentAncestor: ViewEnvironmentPropagating? { parent ?? presentingViewController } 25 | 26 | @_spi(ViewEnvironmentWiring) 27 | public var defaultEnvironmentDescendants: [ViewEnvironmentPropagating] { 28 | var descendants = children 29 | 30 | if let presentedViewController { 31 | descendants.append(presentedViewController) 32 | } 33 | 34 | return descendants 35 | } 36 | 37 | @_spi(ViewEnvironmentWiring) 38 | public func setNeedsApplyEnvironment() { 39 | viewIfLoaded?.setNeedsLayout() 40 | } 41 | } 42 | 43 | #endif 44 | -------------------------------------------------------------------------------- /ViewEnvironmentUI/Tests/ViewEnvironment+Test.swift: -------------------------------------------------------------------------------- 1 | import ViewEnvironment 2 | 3 | public struct TestContext: Equatable { 4 | static var nonDefault: Self { 5 | .init( 6 | number: 999, 7 | string: "Lorem ipsum", 8 | bool: true 9 | ) 10 | } 11 | 12 | var number: Int = 0 13 | var string: String = "" 14 | var bool: Bool = false 15 | } 16 | 17 | public struct TestContextKey: ViewEnvironmentKey { 18 | public static var defaultValue: TestContext { .init() } 19 | } 20 | 21 | extension ViewEnvironment { 22 | var testContext: TestContext { 23 | get { self[TestContextKey.self] } 24 | set { self[TestContextKey.self] = newValue } 25 | } 26 | } 27 | 28 | extension ViewEnvironment { 29 | static var nonDefault: Self { 30 | var environment = Self.empty 31 | environment.testContext = .nonDefault 32 | return environment 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Workflow/Sources/DispatchQueue+Workflow.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import Foundation 18 | 19 | extension DispatchQueue { 20 | @_spi(WorkflowInternals) 21 | public static let workflowExecution: DispatchQueue = .main 22 | } 23 | -------------------------------------------------------------------------------- /Workflow/Sources/Lifetime.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import Foundation 18 | 19 | /// Represents the lifetime of an object. 20 | /// 21 | /// Once ended, the `onEnded` closure is called. 22 | public final class Lifetime { 23 | /// Hook to clean-up after end of `lifetime`. 24 | public func onEnded(_ action: @escaping () -> Void) { 25 | assert(!hasEnded, "Lifetime used after being ended.") 26 | onEndedActions.append(action) 27 | } 28 | 29 | public private(set) var hasEnded: Bool = false 30 | private var onEndedActions: [() -> Void] = [] 31 | 32 | deinit { 33 | end() 34 | } 35 | 36 | func end() { 37 | guard !hasEnded else { 38 | return 39 | } 40 | hasEnded = true 41 | onEndedActions.forEach { $0() } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Workflow/Sources/Sink.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /// Sink is a type that receives incoming values (commonly events or `WorkflowAction`) 18 | /// 19 | /// Use `RenderContext.makeSink` to create instances. 20 | public struct Sink { 21 | private let onValue: (Value) -> Void 22 | 23 | /// Initializes a new sink with the given closure. 24 | public init(_ onValue: @escaping (Value) -> Void) { 25 | self.onValue = onValue 26 | } 27 | 28 | /// Sends a new event into the sink. 29 | /// 30 | /// - Parameter event: The value to send into the sink. 31 | public func send(_ value: Value) { 32 | onValue(value) 33 | } 34 | 35 | /// Generates a new sink of type NewValue. 36 | /// 37 | /// Given a `transform` closure, the following code is functionally equivalent: 38 | /// 39 | /// ``` 40 | /// sink.send(transform(value)) 41 | /// ``` 42 | /// ``` 43 | /// sink.contraMap(transform).send(value) 44 | /// ``` 45 | /// 46 | /// **Trivia**: Why is this called `contraMap`? 47 | /// - `map` turns `Type` into `Type` via `(T)->U`. 48 | /// - `contraMap` turns `Type` into `Type` via `(U)->T` 49 | /// 50 | /// Another way to think about this is: `map` transforms a type by changing the 51 | /// output types of its API, while `contraMap` transforms a type by changing the 52 | /// *input* types of its API. 53 | /// 54 | /// - Parameter transform: An escaping closure that transforms `T` into `Event`. 55 | public func contraMap(_ transform: @escaping (NewValue) -> Value) -> Sink { 56 | Sink { value in 57 | send(transform(value)) 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Workflow/Sources/StateMutationSink.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import Foundation 18 | 19 | extension RenderContext { 20 | /// Creates `StateMutationSink`. 21 | /// 22 | /// To create a sink: 23 | /// ``` 24 | /// let stateMutationSink = context.makeStateMutationSink() 25 | /// ``` 26 | /// 27 | /// To mutate `State` on an event: 28 | /// ``` 29 | /// stateMutationSink.send(\State.value, value: 10) 30 | /// ``` 31 | public func makeStateMutationSink() -> StateMutationSink { 32 | let sink = makeSink(of: AnyWorkflowAction.self) 33 | return StateMutationSink(sink) 34 | } 35 | } 36 | 37 | /// StateMutationSink provides a `Sink` that helps mutate `State` using it's `KeyPath`. 38 | public struct StateMutationSink { 39 | let sink: Sink> 40 | 41 | /// Sends message to `StateMutationSink` to update `State`'s value using the provided closure. 42 | /// 43 | /// - Parameters: 44 | /// - update: The `State` mutation to perform. 45 | public func send(_ update: @escaping (inout WorkflowType.State) -> Void) { 46 | sink.send( 47 | AnyWorkflowAction { state, _ in 48 | update(&state) 49 | return nil 50 | } 51 | ) 52 | } 53 | 54 | /// Sends message to `StateMutationSink` to update `State`'s value at `KeyPath` with `Value`. 55 | /// 56 | /// - Parameters: 57 | /// - keyPath: Key path of `State` whose value needs to be mutated. 58 | /// - value: Value to update `State` with. 59 | public func send(_ keyPath: WritableKeyPath, value: Value) { 60 | send { $0[keyPath: keyPath] = value } 61 | } 62 | 63 | init(_ sink: Sink>) { 64 | self.sink = sink 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Workflow/Tests/ApplyContextTests.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import Testing 18 | 19 | @testable import Workflow 20 | 21 | @MainActor 22 | struct ApplyContextTests { 23 | @Test 24 | func concreteApplyContextInvalidatedAfterUse() async throws { 25 | var escapedContext: ApplyContext? 26 | let onApply = { (context: ApplyContext) in 27 | #expect(context[workflowValue: \.property] == 42) 28 | #expect(context.concreteStorage != nil) 29 | escapedContext = context 30 | } 31 | 32 | let workflow = EscapingContextWorkflow( 33 | property: 42, 34 | onApply: onApply 35 | ) 36 | let node = WorkflowNode(workflow: workflow) 37 | 38 | let emitEvent = node.render() 39 | node.enableEvents() 40 | 41 | emitEvent() 42 | 43 | #expect(escapedContext != nil) 44 | #expect(escapedContext?.concreteStorage == nil) 45 | } 46 | } 47 | 48 | // MARK: - 49 | 50 | private struct EscapingContextWorkflow: Workflow { 51 | typealias Rendering = () -> Void 52 | typealias State = Void 53 | 54 | var property: Int 55 | var onApply: ((ApplyContext) -> Void)? 56 | 57 | func render( 58 | state: State, 59 | context: RenderContext 60 | ) -> Rendering { 61 | let sink = context.makeSink(of: EscapingAction.self) 62 | let action = EscapingAction(onApply: onApply) 63 | return { sink.send(action) } 64 | } 65 | 66 | struct EscapingAction: WorkflowAction { 67 | typealias WorkflowType = EscapingContextWorkflow 68 | 69 | var onApply: ((ApplyContext) -> Void)? 70 | 71 | func apply( 72 | toState state: inout WorkflowType.State, 73 | context: ApplyContext 74 | ) -> WorkflowType.Output? { 75 | onApply?(context) 76 | return nil 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Workflow/Tests/DebuggingTests.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import XCTest 18 | @testable import Workflow 19 | 20 | public class DebuggingTests: XCTestCase { 21 | func test_debugTreeCoding() { 22 | let tree = WorkflowHierarchyDebugSnapshot( 23 | workflowType: "foo", 24 | stateDescription: "bar", 25 | children: [ 26 | WorkflowHierarchyDebugSnapshot.Child( 27 | key: "a", 28 | snapshot: WorkflowHierarchyDebugSnapshot( 29 | workflowType: "hello", 30 | stateDescription: "world" 31 | ) 32 | ), 33 | WorkflowHierarchyDebugSnapshot.Child( 34 | key: "b", 35 | snapshot: WorkflowHierarchyDebugSnapshot( 36 | workflowType: "testing", 37 | stateDescription: "123" 38 | ) 39 | ), 40 | ] 41 | ) 42 | 43 | let encoded = try! JSONEncoder().encode(tree) 44 | let decoded = try! JSONDecoder().decode(WorkflowHierarchyDebugSnapshot.self, from: encoded) 45 | 46 | XCTAssertEqual(tree, decoded) 47 | } 48 | 49 | func test_debugUpdateInfoCoding() { 50 | let info = WorkflowUpdateDebugInfo( 51 | workflowType: "foo", 52 | kind: .didUpdate( 53 | source: .subtree( 54 | WorkflowUpdateDebugInfo( 55 | workflowType: "baz", 56 | kind: .didUpdate(source: .external) 57 | ) 58 | )) 59 | ) 60 | 61 | let encoded = try! JSONEncoder().encode(info) 62 | let decoded = try! JSONDecoder().decode(WorkflowUpdateDebugInfo.self, from: encoded) 63 | 64 | XCTAssertEqual(info, decoded) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Workflow/Tests/HostContextTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | @testable import Workflow 4 | 5 | final class HostContextTests: XCTestCase { 6 | func test_conditional_debug_info_no_debugger() { 7 | let subject = HostContext.testing(debugger: nil) 8 | subject.ifDebuggerEnabled { 9 | XCTFail("should not be called") 10 | } 11 | } 12 | 13 | func test_conditional_debug_info_with_debugger() { 14 | let subject = HostContext.testing(debugger: TestDebugger()) 15 | let expectation = expectation(description: "debugger block invoked") 16 | 17 | subject.ifDebuggerEnabled { 18 | expectation.fulfill() 19 | } 20 | 21 | wait(for: [expectation], timeout: 0.001) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /WorkflowCombine/Sources/Logger.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import os.signpost 18 | @_spi(Logging) import Workflow 19 | 20 | extension OSLog { 21 | fileprivate static let worker = OSLog(subsystem: "com.squareup.WorkflowCombine", category: "Worker") 22 | } 23 | 24 | /// Logs Worker events to OSLog 25 | final class WorkerLogger { 26 | init() {} 27 | 28 | var signpostID: OSSignpostID { OSSignpostID(log: .worker, object: self) } 29 | 30 | // MARK: - Workers 31 | 32 | func logStarted() { 33 | guard WorkflowLogging.isOSLoggingAllowed else { return } 34 | 35 | os_signpost( 36 | .begin, 37 | log: .worker, 38 | name: "Running", 39 | signpostID: signpostID, 40 | "Worker: %{private}@", 41 | String(describing: WorkerType.self) 42 | ) 43 | } 44 | 45 | func logFinished(status: StaticString) { 46 | guard WorkflowLogging.isOSLoggingAllowed else { return } 47 | 48 | os_signpost( 49 | .end, 50 | log: .worker, 51 | name: "Running", 52 | signpostID: signpostID, 53 | status 54 | ) 55 | } 56 | 57 | func logOutput() { 58 | guard WorkflowLogging.isOSLoggingAllowed else { return } 59 | 60 | os_signpost( 61 | .event, 62 | log: .worker, 63 | name: "Worker Event", 64 | signpostID: signpostID, 65 | "Event: %{private}@", 66 | String(describing: WorkerType.self) 67 | ) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /WorkflowCombine/Sources/Publisher+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Publisher+Extensions.swift 3 | // WorkflowCombine 4 | // 5 | // Created by Soo Rin Park on 11/3/21. 6 | // 7 | 8 | #if canImport(Combine) 9 | 10 | import Combine 11 | import Foundation 12 | import Workflow 13 | 14 | /// This is a workaround to the fact you extensions of protocols cannot have an inheritance clause. 15 | /// a previous solution had extending the `AnyPublisher` to conform to `AnyWorkflowConvertible`, 16 | /// but was limited in the fact that rendering was only available to `AnyPublisher`s. 17 | /// this solutions makes it so that all publishers can render its view. 18 | extension Publisher where Failure == Never { 19 | public func running(in context: RenderContext, key: String = "") where 20 | Output == AnyWorkflowAction 21 | { 22 | asAnyWorkflow().rendered(in: context, key: key, outputMap: { $0 }) 23 | } 24 | 25 | public func mapOutput(_ transform: @escaping (Output) -> NewOutput) -> AnyWorkflow { 26 | asAnyWorkflow().mapOutput(transform) 27 | } 28 | 29 | public func asAnyWorkflow() -> AnyWorkflow { 30 | PublisherWorkflow(publisher: self).asAnyWorkflow() 31 | } 32 | } 33 | 34 | #endif 35 | -------------------------------------------------------------------------------- /WorkflowCombine/Sources/PublisherWorkflow.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #if canImport(Combine) 18 | 19 | import Combine 20 | import Foundation 21 | import Workflow 22 | 23 | struct PublisherWorkflow: Workflow where WorkflowPublisher.Failure == Never { 24 | typealias Output = WorkflowPublisher.Output 25 | typealias State = Void 26 | typealias Rendering = Void 27 | 28 | let publisher: WorkflowPublisher 29 | 30 | init(publisher: WorkflowPublisher) { 31 | self.publisher = publisher 32 | } 33 | 34 | func render(state: State, context: RenderContext) -> Rendering { 35 | let sink = context.makeSink(of: AnyWorkflowAction.self) 36 | context.runSideEffect(key: "") { [publisher] lifetime in 37 | let cancellable = publisher 38 | .map { AnyWorkflowAction(sendingOutput: $0) } 39 | .receive(on: DispatchQueue.main) 40 | .sink { sink.send($0) } 41 | 42 | lifetime.onEnded { 43 | cancellable.cancel() 44 | } 45 | } 46 | } 47 | } 48 | 49 | #endif 50 | -------------------------------------------------------------------------------- /WorkflowCombine/Testing/PublisherTesting.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #if DEBUG 18 | 19 | import Combine 20 | import Workflow 21 | import WorkflowTesting 22 | import XCTest 23 | @testable import WorkflowCombine 24 | 25 | extension RenderTester { 26 | /// Expect a `Publisher`-based Workflow. 27 | /// 28 | /// `PublisherWorkflow` is used to subscribe to `Publisher`s. 29 | /// 30 | /// - Parameters: 31 | /// - publisher: Type of the Publisher-based Workflow to expect 32 | /// - producingOutput: An output that will be returned when this worker is requested, if any. 33 | /// - key: Key to expect this `Workflow` to be rendered with. 34 | public func expect( 35 | publisher: PublisherType.Type, 36 | producingOutput output: PublisherType.Output? = nil, 37 | key: String = "" 38 | ) -> RenderTester where PublisherType.Failure == Never { 39 | expectWorkflow( 40 | type: PublisherWorkflow.self, 41 | key: key, 42 | producingRendering: (), 43 | producingOutput: output, 44 | assertions: { _ in } 45 | ) 46 | } 47 | 48 | @available(*, deprecated, renamed: "expect(publisher:producingOutput:key:)") 49 | public func expect( 50 | publisher: PublisherType.Type, 51 | output: PublisherType.Output, 52 | key: String = "" 53 | ) -> RenderTester where PublisherType.Failure == Never { 54 | expect( 55 | publisher: publisher, 56 | producingOutput: output, 57 | key: key 58 | ) 59 | } 60 | } 61 | 62 | #endif 63 | -------------------------------------------------------------------------------- /WorkflowCombine/TestingTests/PublisherTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PublisherTests.swift 3 | // WorkflowCombine 4 | // 5 | // Created by Soo Rin Park on 11/3/21. 6 | // 7 | 8 | import Combine 9 | import Foundation 10 | import Workflow 11 | import WorkflowTesting 12 | import XCTest 13 | @testable import WorkflowCombineTesting 14 | 15 | class PublisherTests: XCTestCase { 16 | func testPublisherWorkflow() { 17 | TestWorkflow() 18 | .renderTester() 19 | .expect( 20 | publisher: Publishers.Sequence<[Int], Never>.self, 21 | producingOutput: 1, 22 | key: "123" 23 | ) 24 | .render {} 25 | } 26 | 27 | func test_publisher_no_output() { 28 | TestWorkflow() 29 | .renderTester() 30 | .expect( 31 | publisher: Publishers.Sequence<[Int], Never>.self, 32 | producingOutput: nil, 33 | key: "123" 34 | ) 35 | .render {} 36 | .assertNoAction() 37 | } 38 | 39 | struct TestWorkflow: Workflow { 40 | typealias State = Void 41 | typealias Rendering = Void 42 | 43 | func render(state: State, context: RenderContext) -> Rendering { 44 | [1].publisher 45 | .mapOutput { _ in AnyWorkflowAction.noAction } 46 | .running(in: context, key: "123") 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /WorkflowConcurrency/Sources/AsyncOperationWorker.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import Foundation 18 | import Workflow 19 | 20 | /// Convenience to execute an async function in a worker. 21 | /// 22 | /// Example of using an async function. 23 | /// ``` 24 | /// func render(state: State, context: RenderContext) -> MyScreen { 25 | /// AsyncOperationWorker(myAsyncFunction) 26 | /// .mapOutput { MyAction($0) } 27 | /// .running(in: context, key: "UniqueKey") 28 | /// 29 | /// return MyScreen() 30 | /// } 31 | /// ``` 32 | /// 33 | /// Example of using a closure. 34 | /// ``` 35 | /// func render(state: State, context: RenderContext) -> MyScreen { 36 | /// AsyncOperationWorker { 37 | /// return await asyncFunctionCall() 38 | /// } 39 | /// .mapOutput { MyAction($0) } 40 | /// .running(in: context, key: "UniqueKey") 41 | /// 42 | /// return MyScreen() 43 | /// } 44 | /// ``` 45 | 46 | public struct AsyncOperationWorker: Worker { 47 | private let operation: () async -> OutputType 48 | 49 | public init(_ operation: @escaping () async -> OutputType) { 50 | self.operation = operation 51 | } 52 | 53 | public func run() async -> OutputType { 54 | await operation() 55 | } 56 | 57 | public typealias Output = OutputType 58 | 59 | public func isEquivalent(to otherWorker: AsyncOperationWorker) -> Bool { 60 | true 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /WorkflowConcurrency/Sources/Logger.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import os.signpost 18 | @_spi(Logging) import Workflow 19 | 20 | extension OSLog { 21 | fileprivate static let worker = OSLog(subsystem: "com.squareup.WorkflowConcurrency", category: "Worker") 22 | } 23 | 24 | // Logs Worker events to OSLog 25 | final class WorkerLogger { 26 | init() {} 27 | 28 | var signpostID: OSSignpostID { OSSignpostID(log: .worker, object: self) } 29 | 30 | // MARK: - Workers 31 | 32 | func logStarted() { 33 | guard WorkflowLogging.isOSLoggingAllowed else { return } 34 | 35 | os_signpost( 36 | .begin, 37 | log: .worker, 38 | name: "Running", 39 | signpostID: signpostID, 40 | "Worker: %{private}@", 41 | String(describing: WorkerType.self) 42 | ) 43 | } 44 | 45 | func logFinished(status: StaticString) { 46 | guard WorkflowLogging.isOSLoggingAllowed else { return } 47 | 48 | os_signpost( 49 | .end, 50 | log: .worker, 51 | name: "Running", 52 | signpostID: signpostID, 53 | status 54 | ) 55 | } 56 | 57 | func logOutput() { 58 | guard WorkflowLogging.isOSLoggingAllowed else { return } 59 | 60 | os_signpost( 61 | .event, 62 | log: .worker, 63 | name: "Worker Event", 64 | signpostID: signpostID, 65 | "Event: %{private}@", 66 | String(describing: WorkerType.self) 67 | ) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /WorkflowReactiveSwift/Sources/Logger.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import os.signpost 18 | @_spi(Logging) import Workflow 19 | 20 | // Namespace for Worker logging 21 | public enum WorkerLogging {} 22 | 23 | extension WorkerLogging { 24 | public static var enabled: Bool { 25 | get { OSLog.active === OSLog.worker } 26 | set { 27 | guard WorkflowLogging.isOSLoggingAllowed else { return } 28 | OSLog.active = newValue ? .worker : .disabled 29 | } 30 | } 31 | } 32 | 33 | extension OSLog { 34 | fileprivate static let worker = OSLog(subsystem: "com.squareup.WorkflowReactiveSwift", category: "Worker") 35 | 36 | fileprivate static var active: OSLog = WorkflowLogging.isOSLoggingAllowed ? .worker : .disabled 37 | } 38 | 39 | // MARK: - 40 | 41 | /// Logs Worker events to OSLog 42 | final class WorkerLogger { 43 | init() {} 44 | 45 | var signpostID: OSSignpostID { OSSignpostID(log: .active, object: self) } 46 | 47 | // MARK: - Workers 48 | 49 | func logStarted() { 50 | guard WorkerLogging.enabled else { return } 51 | 52 | os_signpost( 53 | .begin, 54 | log: .active, 55 | name: "Running", 56 | signpostID: signpostID, 57 | "Worker: %{private}@", 58 | String(describing: WorkerType.self) 59 | ) 60 | } 61 | 62 | func logFinished(status: StaticString) { 63 | guard WorkerLogging.enabled else { return } 64 | 65 | os_signpost(.end, log: .active, name: "Running", signpostID: signpostID, status) 66 | } 67 | 68 | func logOutput() { 69 | guard WorkerLogging.enabled else { return } 70 | 71 | os_signpost( 72 | .event, 73 | log: .active, 74 | name: "Worker Event", 75 | signpostID: signpostID, 76 | "Event: %{private}@", 77 | String(describing: WorkerType.self) 78 | ) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /WorkflowReactiveSwift/Sources/QueueScheduler+Workflow.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import Foundation 18 | import ReactiveSwift 19 | 20 | @_spi(WorkflowInternals) import Workflow 21 | 22 | extension QueueScheduler { 23 | static let workflowExecution: QueueScheduler = .init( 24 | qos: .userInteractive, 25 | name: "com.squareup.workflow", 26 | targeting: DispatchQueue.workflowExecution 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /WorkflowReactiveSwift/Sources/SignalWorker.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import ReactiveSwift 18 | import Workflow 19 | 20 | /// Convenience to use `Signal` as a `Workflow` 21 | /// 22 | /// `Output` of this `Workflow` can be mapped to a `WorkflowAction`. 23 | /// 24 | /// - Important: 25 | /// In a `render()` call, if running multiple `Signal` or if a 26 | /// `Signal` can change in-between render passes, use a `Worker` 27 | /// instead or use an explicit `key` while `running`. 28 | /// 29 | /// ``` 30 | /// func render(state: State, context: RenderContext) -> MyScreen { 31 | /// signal 32 | /// .mapOutput { MyAction($0) } 33 | /// .running(in: context, key: "UniqueKeyForSignal") 34 | /// 35 | /// return MyScreen() 36 | /// } 37 | /// ``` 38 | extension Signal: AnyWorkflowConvertible where Error == Never { 39 | public func asAnyWorkflow() -> AnyWorkflow { 40 | SignalProducerWorkflow(signalProducer: SignalProducer(self)).asAnyWorkflow() 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /WorkflowReactiveSwift/Testing/SignalProducerWorkflowTesting.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #if DEBUG 18 | import Workflow 19 | import WorkflowTesting 20 | import XCTest 21 | @testable import WorkflowReactiveSwift 22 | 23 | extension RenderTester { 24 | /// Expect a `SignalProducer` with an optional output. 25 | /// 26 | /// `SignalProducerWorkflow` is used to subscribe to `SignalProducer`s and `Signal`s. 27 | /// 28 | /// ⚠️ N.B. If you are testing a case in which multiple `SignalProducerWorkflow`s are expected, **only one of them** may have a non-nil `producingOutput` parameter. 29 | /// 30 | /// - Parameters: 31 | /// - outputType: The `OutputType` of the expected `SignalProducerWorkflow`. Typically this will be correctly inferred by the type system, but may need to be explicitly specified if particular optionality is desired. 32 | /// - producingOutput: An output that will be returned when this worker is requested, if any. 33 | /// - key: Key to expect this `Workflow` to be rendered with. 34 | public func expectSignalProducer( 35 | outputType: OutputType.Type = OutputType.self, 36 | producingOutput: OutputType? = nil, 37 | key: String = "", 38 | file: StaticString = #file, line: UInt = #line 39 | ) -> RenderTester { 40 | expectWorkflow( 41 | type: SignalProducerWorkflow.self, 42 | key: key, 43 | producingRendering: (), 44 | producingOutput: producingOutput, 45 | assertions: { _ in } 46 | ) 47 | } 48 | } 49 | #endif 50 | -------------------------------------------------------------------------------- /WorkflowRxSwift/Sources/Logger.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import os.signpost 18 | @_spi(Logging) import Workflow 19 | 20 | // Namespace for Worker logging 21 | public enum WorkerLogging {} 22 | 23 | extension WorkerLogging { 24 | public static var enabled: Bool { 25 | get { OSLog.active === OSLog.worker } 26 | set { 27 | guard WorkflowLogging.isOSLoggingAllowed else { return } 28 | OSLog.active = newValue ? .worker : .disabled 29 | } 30 | } 31 | } 32 | 33 | extension OSLog { 34 | fileprivate static let worker = OSLog(subsystem: "com.squareup.WorkflowRxSwift", category: "Worker") 35 | 36 | fileprivate static var active: OSLog = WorkflowLogging.isOSLoggingAllowed ? .worker : .disabled 37 | } 38 | 39 | // MARK: - 40 | 41 | /// Logs Worker events to OSLog 42 | final class WorkerLogger { 43 | init() {} 44 | 45 | var signpostID: OSSignpostID { OSSignpostID(log: .active, object: self) } 46 | 47 | // MARK: - Workers 48 | 49 | func logStarted() { 50 | guard WorkerLogging.enabled else { return } 51 | 52 | os_signpost( 53 | .begin, 54 | log: .active, 55 | name: "Running", 56 | signpostID: signpostID, 57 | "Worker: %{private}@", 58 | String(describing: WorkerType.self) 59 | ) 60 | } 61 | 62 | func logFinished(status: StaticString) { 63 | guard WorkerLogging.enabled else { return } 64 | 65 | os_signpost(.end, log: .active, name: "Running", signpostID: signpostID, status) 66 | } 67 | 68 | func logOutput() { 69 | guard WorkerLogging.enabled else { return } 70 | 71 | os_signpost( 72 | .event, 73 | log: .active, 74 | name: "Worker Event", 75 | signpostID: signpostID, 76 | "Event: %{private}@", 77 | String(describing: WorkerType.self) 78 | ) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /WorkflowRxSwift/Sources/ObservableWorkflow.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import RxSwift 18 | import Workflow 19 | import class Workflow.Lifetime 20 | 21 | extension Observable: AnyWorkflowConvertible { 22 | public func asAnyWorkflow() -> AnyWorkflow { 23 | ObservableWorkflow(observable: self).asAnyWorkflow() 24 | } 25 | } 26 | 27 | struct ObservableWorkflow: Workflow { 28 | public typealias Output = Value 29 | public typealias State = Void 30 | public typealias Rendering = Void 31 | 32 | var observable: Observable 33 | 34 | public init(observable: Observable) { 35 | self.observable = observable 36 | } 37 | 38 | public func render(state: State, context: RenderContext) -> Rendering { 39 | let sink = context.makeSink(of: AnyWorkflowAction.self) 40 | context.runSideEffect(key: "") { [observable] lifetime in 41 | let disposable = observable 42 | .map { AnyWorkflowAction(sendingOutput: $0) } 43 | .subscribe(on: MainScheduler.asyncInstance) 44 | .observe(on: MainScheduler.asyncInstance) 45 | .subscribe(onNext: { value in 46 | sink.send(value) 47 | }) 48 | 49 | lifetime.onEnded { 50 | disposable.dispose() 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /WorkflowRxSwift/Testing/ObservableTesting.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #if DEBUG 18 | import Workflow 19 | import WorkflowTesting 20 | import XCTest 21 | @testable import WorkflowRxSwift 22 | 23 | extension RenderTester { 24 | /// Expect the given worker. It will be checked for `isEquivalent(to:)` with the requested worker. 25 | 26 | /// - Parameters: 27 | /// - outputType: The `OutputType` of the expected `ObservableWorkflow`. 28 | /// - producingOutput: An output that will be returned when this worker is requested, if any. 29 | /// - key: Key to expect this `Workflow` to be rendered with. 30 | public func expectObservable( 31 | outputType: OutputType.Type = OutputType.self, 32 | producingOutput output: OutputType? = nil, 33 | key: String = "", 34 | file: StaticString = #file, line: UInt = #line 35 | ) -> RenderTester { 36 | expectWorkflow( 37 | type: ObservableWorkflow.self, 38 | key: key, 39 | producingRendering: (), 40 | producingOutput: output, 41 | assertions: { _ in } 42 | ) 43 | } 44 | } 45 | #endif 46 | -------------------------------------------------------------------------------- /WorkflowRxSwift/TestingTests/ObservableTests.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import Foundation 18 | import RxSwift 19 | import Workflow 20 | import WorkflowRxSwiftTesting 21 | import XCTest 22 | 23 | class ObservableTests: XCTestCase { 24 | func testObservableWorkflow() { 25 | TestWorkflow() 26 | .renderTester() 27 | .expectObservable(producingOutput: 1, key: "123") 28 | .render {} 29 | } 30 | 31 | func test_observableWorkflow_optionalOutputType() { 32 | OptionalOutputWorkflow() 33 | .renderTester() 34 | .expectObservable( 35 | outputType: Int?.self, // comment this out & test fails 36 | producingOutput: nil as Int?, 37 | key: "123" 38 | ) 39 | .render {} 40 | } 41 | 42 | private struct TestWorkflow: Workflow { 43 | typealias State = Void 44 | typealias Rendering = Void 45 | 46 | func render(state: State, context: RenderContext) -> Rendering { 47 | Observable.from([1]) 48 | .mapOutput { _ in AnyWorkflowAction.noAction } 49 | .running(in: context, key: "123") 50 | } 51 | } 52 | 53 | private struct OptionalOutputWorkflow: Workflow { 54 | typealias State = Void 55 | typealias Rendering = Void 56 | typealias Output = Int? 57 | 58 | func render(state: State, context: RenderContext) -> Rendering { 59 | Observable.from([1]) 60 | .map { Int?.some($0) } 61 | .mapOutput { _ in AnyWorkflowAction.noAction } 62 | .rendered(in: context, key: "123") 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /WorkflowSwiftUI/Sources/ActionModel.swift: -------------------------------------------------------------------------------- 1 | /// An ``ObservableModel`` for workflows with a single action. 2 | /// 3 | /// To create an accessor, use 4 | /// ``Workflow/RenderContext/makeActionModel(state:)``. State writes and actions 5 | /// will be sent to the workflow. 6 | public struct ActionModel: ObservableModel, SingleActionModel { 7 | public let accessor: StateAccessor 8 | public let sendAction: (Action) -> Void 9 | 10 | /// Creates a new ActionModel. 11 | /// 12 | /// Rather than creating this model directly, you should usually use the 13 | /// ``Workflow/RenderContext/makeActionModel(state:)`` method to create an 14 | /// instance of this model. If you need a static model for testing or 15 | /// previews, you can use the ``constant(state:)`` method. 16 | public init(accessor: StateAccessor, sendAction: @escaping (Action) -> Void) { 17 | self.accessor = accessor 18 | self.sendAction = sendAction 19 | } 20 | } 21 | 22 | /// An observable model with a single action. 23 | /// 24 | /// Conforming to this type provides some convenience methods for sending actions to the model. You 25 | /// can use ``ActionModel`` rather than conforming yourself. 26 | public protocol SingleActionModel: ObservableModel { 27 | associatedtype Action 28 | 29 | var sendAction: (Action) -> Void { get } 30 | } 31 | 32 | extension ActionModel: Identifiable where State: Identifiable { 33 | public var id: State.ID { 34 | accessor.id 35 | } 36 | } 37 | 38 | #if DEBUG 39 | 40 | extension ActionModel { 41 | /// Creates a static model which ignores all sent values, suitable for static previews 42 | /// or testing. 43 | public static func constant(state: State) -> ActionModel { 44 | ActionModel(accessor: .constant(state: state), sendAction: { _ in }) 45 | } 46 | } 47 | 48 | #endif 49 | -------------------------------------------------------------------------------- /WorkflowSwiftUI/Sources/Derived/AreOrderedSetsDuplicates.swift: -------------------------------------------------------------------------------- 1 | // Derived from 2 | // https://github.com/pointfreeco/swift-composable-architecture/blob/1.12.1/Sources/ComposableArchitecture/Internal/AreOrderedSetsDuplicates.swift 3 | 4 | import Foundation 5 | import OrderedCollections 6 | 7 | @inlinable 8 | func areOrderedSetsDuplicates(_ lhs: OrderedSet, _ rhs: OrderedSet) -> Bool { 9 | guard lhs.count == rhs.count 10 | else { return false } 11 | 12 | return withUnsafePointer(to: lhs) { lhsPointer in 13 | withUnsafePointer(to: rhs) { rhsPointer in 14 | memcmp(lhsPointer, rhsPointer, MemoryLayout>.size) == 0 || lhs == rhs 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /WorkflowSwiftUI/Sources/Exports.swift: -------------------------------------------------------------------------------- 1 | #if canImport(Observation) 2 | @_exported import Observation 3 | #endif 4 | #if canImport(Perception) 5 | @_exported import Perception 6 | #endif 7 | -------------------------------------------------------------------------------- /WorkflowSwiftUI/Sources/Macros.swift: -------------------------------------------------------------------------------- 1 | #if swift(>=5.9) 2 | import Observation 3 | 4 | /// Defines and implements conformance of the Observable protocol. 5 | @attached(extension, conformances: Observable, ObservableState) 6 | @attached(member, names: named(_$id), named(_$observationRegistrar), named(_$willModify)) 7 | @attached(memberAttribute) 8 | public macro ObservableState() = 9 | #externalMacro(module: "WorkflowSwiftUIMacros", type: "ObservableStateMacro") 10 | 11 | @attached(accessor, names: named(init), named(get), named(set)) 12 | @attached(peer, names: prefixed(_)) 13 | public macro ObservationStateTracked() = 14 | #externalMacro(module: "WorkflowSwiftUIMacros", type: "ObservationStateTrackedMacro") 15 | 16 | @attached(accessor, names: named(willSet)) 17 | public macro ObservationStateIgnored() = 18 | #externalMacro(module: "WorkflowSwiftUIMacros", type: "ObservationStateIgnoredMacro") 19 | 20 | #endif 21 | -------------------------------------------------------------------------------- /WorkflowSwiftUI/Sources/RenderContext+ObservableModel.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Workflow 3 | 4 | extension RenderContext where WorkflowType.State: ObservableState { 5 | /// Creates a ``StateAccessor`` for this workflow's state. 6 | /// 7 | /// ``StateAccessor`` is used by ``ObservableModel`` to read and write observable state. A state 8 | /// accessor can serve as the ``ObservableModel`` implementation for simple workflows with no 9 | /// actions. State updates will be sent to the workflow's state mutation sink. 10 | public func makeStateAccessor( 11 | state: WorkflowType.State 12 | ) -> StateAccessor { 13 | StateAccessor(state: state, sendValue: makeStateMutationSink().send) 14 | } 15 | 16 | /// Creates an ``ActionModel`` for this workflow's state and action. 17 | /// 18 | /// ``ActionModel`` is a simple ``ObservableModel`` implementation for workflows with one action 19 | /// type. For more complex workflows with multiple actions, you can create a custom model that 20 | /// conforms to ``ObservableModel``. For less complex workflows, you can use 21 | /// ``makeStateAccessor(state:)`` instead. See ``ObservableModel`` for more information. 22 | public func makeActionModel( 23 | state: WorkflowType.State 24 | ) -> ActionModel 25 | where Action.WorkflowType == WorkflowType 26 | { 27 | ActionModel( 28 | accessor: makeStateAccessor(state: state), 29 | sendAction: makeSink(of: Action.self).send 30 | ) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /WorkflowSwiftUI/Sources/StateAccessor.swift: -------------------------------------------------------------------------------- 1 | /// A wrapper around observable state that provides read and write access through unidirectional 2 | /// channels. 3 | /// 4 | /// This type serves as the primary channel of information in an ``ObservableModel``, by providing 5 | /// read and write access to state through separate mechanisms. 6 | /// 7 | /// To create an accessor, use ``Workflow/RenderContext/makeStateAccessor(state:)``. State writes 8 | /// will flow through a workflow's state mutation sink. 9 | /// 10 | /// This type can be embedded in an ``ObservableModel`` or used directly, for trivial workflows with 11 | /// no custom actions. 12 | public struct StateAccessor { 13 | let state: State 14 | let sendValue: (@escaping (inout State) -> Void) -> Void 15 | 16 | /// Creates a new state accessor. 17 | /// 18 | /// Rather than creating this model directly, you should usually use the 19 | /// ``Workflow/RenderContext/makeStateAccessor(state:)`` method. If you need 20 | /// a static model for testing or previews, you can use the 21 | /// ``constant(state:)`` method. 22 | public init( 23 | state: State, 24 | sendValue: @escaping (@escaping (inout State) -> Void) -> Void 25 | ) { 26 | self.state = state 27 | self.sendValue = sendValue 28 | } 29 | } 30 | 31 | extension StateAccessor: ObservableModel { 32 | public var accessor: StateAccessor { self } 33 | } 34 | 35 | extension StateAccessor: Identifiable where State: Identifiable { 36 | public var id: State.ID { 37 | state.id 38 | } 39 | } 40 | 41 | #if DEBUG 42 | 43 | extension StateAccessor { 44 | /// Creates a static state accessor which ignores all sent values, suitable for static previews 45 | /// or testing. 46 | public static func constant(state: State) -> StateAccessor { 47 | StateAccessor(state: state, sendValue: { _ in }) 48 | } 49 | } 50 | 51 | #endif 52 | -------------------------------------------------------------------------------- /WorkflowSwiftUI/Tests/XCTestCase+AppHost.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) 2 | 3 | import UIKit 4 | import XCTest 5 | 6 | extension XCTestCase { 7 | /// Call this method to show a view controller in the test host application during a unit test. 8 | /// 9 | /// After the test runs, the view controller will be removed from the view hierarchy. 10 | /// 11 | /// A test failure will occur if the host application does not exist, or does not have a root 12 | /// view controller. 13 | /// 14 | func show( 15 | viewController: ViewController, 16 | test: (ViewController) throws -> Void 17 | ) rethrows { 18 | guard let rootVC = UIApplication.shared.delegate?.window??.rootViewController else { 19 | #if SWIFT_PACKAGE 20 | print("WARNING: Test cannot run directly from swift, it requires an app host. Please run from the Tuist project.") 21 | #else 22 | XCTFail("Cannot present a view controller in a test host that does not have a root window.") 23 | #endif 24 | return 25 | } 26 | 27 | rootVC.addChild(viewController) 28 | rootVC.view.addSubview(viewController.view) 29 | viewController.didMove(toParent: rootVC) 30 | 31 | try autoreleasepool { 32 | try test(viewController) 33 | } 34 | 35 | viewController.willMove(toParent: nil) 36 | viewController.view.removeFromSuperview() 37 | viewController.removeFromParent() 38 | } 39 | } 40 | 41 | #endif 42 | -------------------------------------------------------------------------------- /WorkflowSwiftUIMacros/Sources/Plugins.swift: -------------------------------------------------------------------------------- 1 | import SwiftCompilerPlugin 2 | import SwiftSyntaxMacros 3 | 4 | @main 5 | struct MacrosPlugin: CompilerPlugin { 6 | let providingMacros: [Macro.Type] = [ 7 | ObservableStateMacro.self, 8 | ObservationStateTrackedMacro.self, 9 | ObservationStateIgnoredMacro.self, 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /WorkflowTesting/Sources/Internal/AppliedAction.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import IssueReporting 18 | import Workflow 19 | import XCTest 20 | 21 | struct AppliedAction { 22 | let erasedAction: Any 23 | 24 | init(_ action: ActionType) where ActionType.WorkflowType == WorkflowType { 25 | self.erasedAction = action 26 | } 27 | 28 | func assert(type: ActionType.Type = ActionType.self, file: StaticString, line: UInt, assertions: (ActionType) throws -> Void) rethrows where ActionType.WorkflowType == WorkflowType { 29 | guard let action = erasedAction as? ActionType else { 30 | reportIssue("Expected action of type \(ActionType.self), got \(erasedAction)", filePath: file, line: line) 31 | return 32 | } 33 | try assertions(action) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /WorkflowTesting/Tests/TestingFrameworkCompatibilityTests.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import Testing 18 | import Workflow 19 | import XCTest 20 | 21 | @testable import WorkflowTesting 22 | 23 | struct SwiftTestingCompatibilityTests { 24 | @Test 25 | func testInternalFailureRecordsExpectationFailure_swiftTesting() { 26 | withKnownIssue { 27 | TestAction 28 | .tester(withState: false) 29 | .send(action: .change(true)) 30 | .assertNoOutput() // should fail the test 31 | } 32 | } 33 | } 34 | 35 | final class XCTestCompatibilityTests: XCTestCase { 36 | func testInternalFailureRecordsExpectationFailure_xctest() { 37 | XCTExpectFailure { 38 | _ = TestAction 39 | .tester(withState: false) 40 | .send(action: .change(true)) 41 | .assertNoOutput() // should fail the test 42 | } 43 | } 44 | } 45 | 46 | private enum TestAction: WorkflowAction { 47 | typealias WorkflowType = TestWorkflow 48 | 49 | case change(Bool) 50 | 51 | func apply(toState state: inout Bool, context: ApplyContext) -> TestWorkflow.Output? { 52 | if case .change(let newState) = self { 53 | state = newState 54 | } 55 | return 42 56 | } 57 | } 58 | 59 | private struct TestWorkflow: Workflow { 60 | typealias Rendering = Void 61 | typealias Output = Int 62 | 63 | func makeInitialState() -> Bool { true } 64 | func render(state: Bool, context: RenderContext) {} 65 | } 66 | -------------------------------------------------------------------------------- /WorkflowUI/Sources/ModuleExports.swift: -------------------------------------------------------------------------------- 1 | @_exported import ViewEnvironment 2 | @_exported import ViewEnvironmentUI 3 | -------------------------------------------------------------------------------- /WorkflowUI/Sources/Observation/WorkflowUIObserver.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #if canImport(UIKit) 18 | import Foundation 19 | 20 | /// Protocol to observe events emitted from WorkflowUI. 21 | /// **N.B. This is currently part of an experimental interface, and may have breaking changes in the future.** 22 | @_spi(ExperimentalObservation) 23 | public protocol WorkflowUIObserver { 24 | func observeEvent(_ event: E) 25 | } 26 | 27 | // MARK: - Global Observation 28 | 29 | @_spi(ExperimentalObservation) 30 | public enum WorkflowUIObservation { 31 | /// The shared `WorkflowUIObserver` instance to which all `WorkflowUIEvent`s will be forwarded. 32 | public static var sharedUIObserver: WorkflowUIObserver? 33 | } 34 | 35 | #endif 36 | -------------------------------------------------------------------------------- /WorkflowUI/Sources/Screen/AnyScreen/AnyScreen.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #if canImport(UIKit) 18 | 19 | import UIKit 20 | 21 | public struct AnyScreen: Screen { 22 | /// The original screen, retained for debugging 23 | public let wrappedScreen: Screen 24 | 25 | public init(_ screen: some Screen) { 26 | if let anyScreen = screen as? AnyScreen { 27 | self = anyScreen 28 | return 29 | } 30 | self.wrappedScreen = screen 31 | } 32 | 33 | public func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { 34 | wrappedScreen.viewControllerDescription(environment: environment) 35 | } 36 | } 37 | 38 | extension Screen { 39 | /// Wraps the screen in an AnyScreen 40 | public func asAnyScreen() -> AnyScreen { 41 | AnyScreen(self) 42 | } 43 | } 44 | 45 | // MARK: SingleScreenContaining 46 | 47 | extension AnyScreen: SingleScreenContaining { 48 | public var primaryScreen: any Screen { 49 | wrappedScreen 50 | } 51 | } 52 | 53 | #endif 54 | -------------------------------------------------------------------------------- /WorkflowUI/Sources/Screen/Screen.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #if canImport(UIKit) 18 | 19 | import UIKit 20 | 21 | /// Screens are the building blocks of an interactive application. 22 | /// 23 | /// Conforming types contain any information needed to populate a screen: data, 24 | /// styling, event handlers, etc. 25 | public protocol Screen { 26 | /// A view controller description that acts as a recipe to either build 27 | /// or update a previously-built view controller to match this screen. 28 | func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription 29 | } 30 | 31 | extension Screen { 32 | /// If the given view controller is of the correct type to be updated by this screen. 33 | /// 34 | /// If your view controller type can change between updates, call this method before invoking `update(viewController:with:)`. 35 | public func canUpdate(viewController: UIViewController, with environment: ViewEnvironment) -> Bool { 36 | viewControllerDescription(environment: environment).canUpdate(viewController: viewController) 37 | } 38 | 39 | /// Update the given view controller with the content from the screen. 40 | /// 41 | /// ### Note 42 | /// You must pass a view controller previously created by a compatible `ViewControllerDescription` 43 | /// that passes `canUpdate(viewController:with:)`. Failure to do so will result in a fatal precondition. 44 | public func update(viewController: UIViewController, with environment: ViewEnvironment) { 45 | viewControllerDescription(environment: environment).update(viewController: viewController) 46 | } 47 | 48 | /// Construct and update a new view controller as described by this Screen. 49 | /// The view controller will be updated before it is returned, so it is fully configured and prepared for display. 50 | public func buildViewController(in environment: ViewEnvironment) -> UIViewController { 51 | viewControllerDescription(environment: environment).buildViewController() 52 | } 53 | } 54 | 55 | #endif 56 | -------------------------------------------------------------------------------- /WorkflowUI/Sources/Screen/ScreenContaining.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) 2 | 3 | import Foundation 4 | import UIKit 5 | 6 | /// Defines a type that semantically 'contains' a `Screen` instance. 7 | /// 8 | /// The motivating use case for this protocol is to expose a means of unifying various types that provide 9 | /// access to `Screen`s in a way that can be used without knowing their static type. 10 | /// 11 | /// For example, without this protocol, we cannot identify that a `UIViewController` is an instance of 12 | /// `ScreenViewController` without specifying the associated screen's type. 13 | /// 14 | /// ```swift 15 | /// func makeUninspectableScreenVC() -> UIViewController { 16 | /// struct LocalScreen: Screen { ... } // `LocalScreen` symbol is only visible in this function 17 | /// return ScreenViewController(screen: LocalScreen(), environment: .empty) 18 | /// } 19 | /// 20 | /// let isSVC = makeUninspectableScreenVC() is ScreenViewController // no generic can be specified here that makes this true 21 | /// ``` 22 | /// 23 | /// Conceptually this API is intended to enable runtime traversal of a hierarchy of `SingleScreenContaining` 24 | /// instances such that an 'inner' `Screen` value can be found at runtime. For example, if we had an instance 25 | /// of `ScreenViewController` but only statically knew it was a `UIViewController`, we 26 | /// can use the conformances to this protocol to conditionally cast & traverse the contained screens to find 27 | /// the inner `wrappedScreen`. 28 | public protocol SingleScreenContaining { 29 | /// The primary `Screen` the conforming type contains. Note that this may be ambiguous in some 30 | /// cases, for instance, if the conforming type logically contains multiple screens. Implementors should 31 | /// return the `Screen` which most appropriately reflects the 'primary' one for a given domain. 32 | var primaryScreen: any Screen { get } 33 | } 34 | 35 | extension SingleScreenContaining { 36 | /// Iteratively traverses a sequence of `primaryScreen` values until one is found that does _not_ 37 | /// conform to `SingleScreenContaining`. Put another way, this returns the first `primaryScreen` 38 | /// that is a `Screen` and _not_ a `SingleScreenContaining` type. 39 | public func findInnermostPrimaryScreen() -> any Screen { 40 | var result = primaryScreen 41 | 42 | while let nextContainer = result as? SingleScreenContaining { 43 | result = nextContainer.primaryScreen 44 | } 45 | 46 | return result 47 | } 48 | } 49 | 50 | #endif 51 | -------------------------------------------------------------------------------- /WorkflowUI/Tests/AdaptedEnvironmentScreenTests.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #if canImport(UIKit) 18 | 19 | import UIKit 20 | import XCTest 21 | @testable import WorkflowUI 22 | 23 | class AdaptedEnvironmentScreenTests: XCTestCase { 24 | func test_wrapping() { 25 | var environment: ViewEnvironment = .empty 26 | 27 | let screen = TestScreen { environment = $0 } 28 | .adaptedEnvironment(key: TestingKey1.self, value: "adapted1.1") 29 | .adaptedEnvironment(key: TestingKey1.self, value: "adapted1.2") 30 | .adaptedEnvironment(key: TestingKey2.self, value: "adapted2.1") 31 | .adaptedEnvironment(key: TestingKey1.self, value: "adapted1.3") 32 | .adaptedEnvironment(key: TestingKey2.self, value: "adapted2.2") 33 | 34 | _ = screen.viewControllerDescription(environment: .empty) 35 | 36 | // The inner-most change; the one closest to the screen; should be the value we get. 37 | XCTAssertEqual(environment[TestingKey1.self], "adapted1.1") 38 | XCTAssertEqual(environment[TestingKey2.self], "adapted2.1") 39 | } 40 | } 41 | 42 | fileprivate enum TestingKey1: ViewEnvironmentKey { 43 | static let defaultValue: String? = nil 44 | } 45 | 46 | fileprivate enum TestingKey2: ViewEnvironmentKey { 47 | static let defaultValue: String? = nil 48 | } 49 | 50 | fileprivate struct TestScreen: Screen { 51 | var read: (ViewEnvironment) -> Void 52 | 53 | func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { 54 | read(environment) 55 | 56 | return ViewController.description(for: self, environment: environment) 57 | } 58 | 59 | private class ViewController: ScreenViewController {} 60 | } 61 | 62 | #endif 63 | -------------------------------------------------------------------------------- /WorkflowUI/Tests/XCTestCase+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XCTestCase+Extensions.swift 3 | // WorkflowUI-Unit-Tests 4 | // 5 | // Created by Kyle Van Essen on 9/1/22. 6 | // 7 | 8 | #if canImport(UIKit) 9 | 10 | import Foundation 11 | import UIKit 12 | import XCTest 13 | 14 | extension XCTestCase { 15 | /// 16 | /// Call this method to show a view controller in the test host application 17 | /// during a unit test. The view controller will be the size of host application's device. 18 | /// 19 | /// After the test runs, the view controller will be removed from the view hierarchy. 20 | /// 21 | /// A test failure will occur if the host application does not exist, or does not have a root view controller. 22 | /// 23 | public func show( 24 | vc viewController: ViewController, 25 | loadAndPlaceView: Bool = true, 26 | test: (ViewController) throws -> Void 27 | ) rethrows { 28 | guard let rootVC = UIApplication.shared.delegate?.window??.rootViewController else { 29 | #if SWIFT_PACKAGE 30 | print("WARNING: Test cannot run in SPM, it requires an app host. Please run WorkflowUI.podspec's tests.") 31 | #else 32 | XCTFail("Cannot present a view controller in a test host that does not have a root window.") 33 | #endif 34 | return 35 | } 36 | 37 | rootVC.addChild(viewController) 38 | viewController.didMove(toParent: rootVC) 39 | 40 | if loadAndPlaceView { 41 | viewController.view.frame = rootVC.view.bounds 42 | viewController.view.layoutIfNeeded() 43 | 44 | rootVC.beginAppearanceTransition(true, animated: false) 45 | rootVC.view.addSubview(viewController.view) 46 | rootVC.endAppearanceTransition() 47 | } 48 | 49 | defer { 50 | if loadAndPlaceView { 51 | viewController.beginAppearanceTransition(false, animated: false) 52 | viewController.view.removeFromSuperview() 53 | viewController.endAppearanceTransition() 54 | } 55 | 56 | viewController.willMove(toParent: nil) 57 | viewController.removeFromParent() 58 | } 59 | 60 | try autoreleasepool { 61 | try test(viewController) 62 | } 63 | } 64 | } 65 | 66 | #endif 67 | --------------------------------------------------------------------------------