├── .github ├── ISSUE_TEMPLATE │ └── feature_request.md └── workflows │ ├── iostest.yml │ └── publish.yml ├── .gitignore ├── Assets └── Images │ ├── Combine Illustrations.graffle │ ├── UsingCombineCover.sketch │ └── UsingCombineWithSwiftGitHubSocial.png ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SwiftUI-Notes.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved └── xcshareddata │ └── xcschemes │ ├── SwiftUI-Notes.xcscheme │ └── UIKit-Combine.xcscheme ├── SwiftUI-Notes ├── AppDelegate.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── Base.lproj │ └── LaunchScreen.storyboard ├── ContentView.swift ├── HeadingView.swift ├── Info.plist ├── LocationModelProxy.swift ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── PublisherView.swift ├── ReactiveForm.swift ├── ReactiveFormModel.swift ├── SampleView.swift ├── SceneDelegate.swift ├── SwiftUI-Notes.entitlements └── SwiftUITabView.swift ├── SwiftUI-NotesTests ├── CombinePatternTests.swift ├── Info.plist ├── SwiftUI_CombineTests.swift └── SwiftUI_NotesTests.swift ├── SwiftUI_IOS_Playground.playground ├── Contents.swift └── contents.xcplayground ├── UIKit-Combine ├── AppDelegate.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── AsyncCoordinatorViewController.swift ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── FormViewController.swift ├── GithubAPI.swift ├── GithubViewController.swift ├── HeadingViewController.swift ├── Info.plist ├── LocationHeadingProxy.swift └── SceneDelegate.swift ├── UIKit-CombineTests ├── Info.plist └── UIKit_CombineTests.swift ├── UsingCombineTests ├── BreakpointPublisherTests.swift ├── ChangingErrorTests.swift ├── CriteriaOperatorTests.swift ├── DataTaskPublisherTests.swift ├── DebounceAndRemoveDuplicatesPublisherTests.swift ├── DebounceAndThrottleTests.swift ├── DeferredPublisherTests.swift ├── EmptyPublisherTests.swift ├── EncodeDecodeTests.swift ├── EntwineTestExampleTests.swift ├── FailedPublisherTests.swift ├── FilterPublisherTests.swift ├── FilteringOperatorTests.swift ├── FuturePublisherTests.swift ├── HandleEventsPublisherTests.swift ├── Info.plist ├── InterimTestingStructs.swift ├── MathOperatorTests.swift ├── MeasureIntervalTests.swift ├── MergeManyPublisherTests.swift ├── MergingPipelineTests.swift ├── Mock.swift ├── Mocker.swift ├── MockingURLProtocol.swift ├── MulticastSharePublisherTests.swift ├── NotificationCenterPublisherTests.swift ├── ObservableObjectPublisherTests.swift ├── PublisherTests.swift ├── RecordPublisherTests.swift ├── ReducingOperatorTests.swift ├── ResultPublisherTests.swift ├── RetryPublisherTests.swift ├── ScanPublisherTests.swift ├── SequencePublisherTests.swift ├── SequentialOperatorTests.swift ├── SinkSubscriberTests.swift ├── SubscribeReceiveAssignTests.swift ├── SwitchAndFlatMapPublisherTests.swift ├── TimerPublisherTests.swift └── XCTestCase+KVOExpectatio0n.swift ├── docs ├── aboutthisbook.adoc ├── coreconcepts.adoc ├── developingwith.adoc ├── images │ ├── UsingCombineWithSwiftCoverArt_16x9.png │ ├── UsingCombineWithSwiftCoverArt_2560x1920.png │ ├── cloneRepository.png │ ├── diagrams │ │ ├── allsatisfy.svg │ │ ├── basic_types.svg │ │ ├── collect.svg │ │ ├── combine_lifecycle_diagram.svg │ │ ├── combine_marble_diagram.svg │ │ ├── combine_pipeline_diagram.svg │ │ ├── compactmap.svg │ │ ├── contains.svg │ │ ├── containswhere.svg │ │ ├── count.svg │ │ ├── debounce.svg │ │ ├── debounce_break.svg │ │ ├── endings.svg │ │ ├── example_map_operator.svg │ │ ├── filter.svg │ │ ├── first.svg │ │ ├── flatmap.svg │ │ ├── input_output.svg │ │ ├── last.svg │ │ ├── map.svg │ │ ├── marble_diagram.svg │ │ ├── max.svg │ │ ├── min.svg │ │ ├── pipeline.svg │ │ ├── reduce.svg │ │ ├── removeduplicates.svg │ │ ├── replaceempty.svg │ │ ├── replacenil.svg │ │ ├── scan.svg │ │ ├── setfailuretype.svg │ │ ├── test_example.svg │ │ ├── throttle_false.svg │ │ ├── throttle_false_13.2.2.svg │ │ ├── throttle_true.svg │ │ ├── throttle_true_13.2.2.svg │ │ └── tryscan.svg │ ├── mars.png │ └── welcomeToXcode.png ├── introduction.adoc ├── lib │ ├── git-metadata-preprocessor.rb │ ├── git-metadata-preprocessor │ │ ├── extension.rb │ │ └── sample.adoc │ └── google-analytics-docinfoprocessor.rb ├── pattern-assertnofailure.adoc ├── pattern-assign.adoc ├── pattern-cascading-update-interface.adoc ├── pattern-constrained-network.adoc ├── pattern-continual-error-handling.adoc ├── pattern-datataskpublisher-decode.adoc ├── pattern-datataskpublisher-trymap.adoc ├── pattern-debugging-pipelines-breakpoint.adoc ├── pattern-debugging-pipelines-handleevents.adoc ├── pattern-debugging-pipelines-print.adoc ├── pattern-delegate-publisher-subject.adoc ├── pattern-future.adoc ├── pattern-merging-streams-interface.adoc ├── pattern-notificationcenter.adoc ├── pattern-observableobject.adoc ├── pattern-oneshot-error-handling.adoc ├── pattern-retry.adoc ├── pattern-sequencing-operations.adoc ├── pattern-sink.adoc ├── pattern-template.adoc ├── pattern-test-entwine.adoc ├── pattern-test-pipeline-expectation.adoc ├── pattern-test-subscriber-scheduled.adoc ├── pattern-test-subscriber-subject.adoc ├── pattern-update-interface-userinput.adoc ├── patterns.adoc ├── raw-notes.adoc ├── reference-template.adoc ├── reference.adoc └── using-combine-book.adoc ├── docs_zh-CN ├── aboutthisbook.adoc ├── coreconcepts.adoc ├── developingwith.adoc ├── introduction.adoc ├── pattern-assertnofailure.adoc ├── pattern-assign.adoc ├── pattern-cascading-update-interface.adoc ├── pattern-constrained-network.adoc ├── pattern-continual-error-handling.adoc ├── pattern-datataskpublisher-decode.adoc ├── pattern-datataskpublisher-trymap.adoc ├── pattern-debugging-pipelines-breakpoint.adoc ├── pattern-debugging-pipelines-handleevents.adoc ├── pattern-debugging-pipelines-print.adoc ├── pattern-delegate-publisher-subject.adoc ├── pattern-future.adoc ├── pattern-merging-streams-interface.adoc ├── pattern-notificationcenter.adoc ├── pattern-observableobject.adoc ├── pattern-oneshot-error-handling.adoc ├── pattern-retry.adoc ├── pattern-sequencing-operations.adoc ├── pattern-sink.adoc ├── pattern-template.adoc ├── pattern-test-entwine.adoc ├── pattern-test-pipeline-expectation.adoc ├── pattern-test-subscriber-scheduled.adoc ├── pattern-test-subscriber-subject.adoc ├── pattern-update-interface-userinput.adoc ├── patterns.adoc ├── raw-notes.adoc ├── reference-template.adoc ├── reference.adoc └── using-combine_zh-CN.adoc ├── marbles ├── README.md ├── diagrams │ ├── newtest.txt │ └── test.txt └── requirements.txt ├── re.bash └── testpages └── swift.adoc /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project/site 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to see added or updated. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/workflows/iostest.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | test-adoc-generation: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | 11 | #- name: pwd 12 | # run: pwd 13 | # result: /home/runner/work/swiftui-notes/swiftui-notes 14 | 15 | - name: verify html generation with asciidoctor 16 | run: docker run --rm -v $(pwd):/documents/ --name asciidoc-to-html heckj/docker-asciidoctor asciidoctor -v -t -D /documents/output -r ./docs/lib/google-analytics-docinfoprocessor.rb docs/using-combine-book.adoc 17 | # results to appear in the directory 'output' 18 | 19 | # build: 20 | 21 | # runs-on: macos-14 22 | #env: 23 | # sets the version of Xcode to utilize within the VM for all steps 24 | # DEVELOPER_DIR: /Applications/Xcode_13.app/Contents/Developer 25 | # steps: 26 | # - uses: actions/checkout@v2 27 | 28 | # - name: docker version 29 | # run: docker -v 30 | 31 | # - name: docker help 32 | # run: docker --help 33 | 34 | # - name: Show what's in Applications 35 | # run: ls -al /Applications 36 | 37 | # - name: xcodebuild --help 38 | # run: xcodebuild --help 39 | 40 | # - name: xcodebuild --showsdks 41 | # run: xcodebuild -showsdks 42 | 43 | # - name: xcodebuild -showBuildSettings 44 | # run: xcodebuild -showBuildSettings 45 | 46 | # - name: xcodebuild -showTestPlans 47 | # run: xcodebuild -showTestPlans 48 | 49 | # - name: xcodebuild -list 50 | # run: xcodebuild -list 51 | 52 | # - name: Show available destinations 53 | # run: xcodebuild -scheme SwiftUI-Notes -showdestinations 54 | 55 | # - name: Run the Combine test suite (iOS) 56 | # run: | 57 | # xcodebuild -scheme SwiftUI-Notes \ 58 | # -configuration Debug \ 59 | # -sdk iphonesimulator17.0 \ 60 | # -destination 'platform=iOS Simulator,OS=17.5,name=iPhone 14' \ 61 | # test -showBuildTimingSummary 62 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | with: 14 | persist-credentials: false 15 | # If you're using actions/checkout@v2 you must set persist-credentials to false 16 | # in most cases for the deployment to work correctly. 17 | 18 | #- name: pwd 19 | # run: pwd 20 | # result: /home/runner/work/swiftui-notes/swiftui-notes 21 | 22 | - name: generate html with asciidoctor from docs/ 23 | run: docker run --rm -v $(pwd):/documents/ --name asciidoc-to-html heckj/docker-asciidoctor asciidoctor -v -t -D /documents/output -r ./docs/lib/google-analytics-docinfoprocessor.rb docs/using-combine-book.adoc 24 | # results to appear in the directory 'output', which on GH action is owned by root, not `me` 25 | 26 | - name: generate zh-CN html with asciidoctor from docs/ 27 | run: docker run --rm -v $(pwd):/documents/ --name asciidoc-to-html heckj/docker-asciidoctor asciidoctor -v -t -D /documents/output -r ./docs/lib/google-analytics-docinfoprocessor.rb docs_zh-CN/using-combine_zh-CN.adoc 28 | # results to appear in the directory 'output', which on GH action is owned by root, not `me` 29 | 30 | - name: permission check 31 | run: ls -altr 32 | 33 | - name: fs-scan output 34 | run: find output 35 | 36 | - name: create build directory and images directory 37 | run: | 38 | mkdir -p build 39 | mkdir -p build/images 40 | 41 | - name: copy images into HTML output directory 42 | run: cp -r docs/images/* build/images 43 | 44 | - name: copy en HTML into build directory 45 | run: cp output/using-combine-book.html build/index.html 46 | 47 | - name: copy zh-CN HTML into build directory 48 | run: cp output/using-combine_zh-CN.html build/index_zh-CN.html 49 | 50 | - name: permission check 51 | run: ls -altr 52 | 53 | - name: fs-scan build 54 | run: find build 55 | 56 | # docs: https://github.com/marketplace/actions/deploy-to-github-pages 57 | - name: Deploy 🚀 58 | uses: JamesIves/github-pages-deploy-action@releases/v3 59 | with: 60 | ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }} 61 | BRANCH: gh-pages # The branch the action should deploy to. 62 | FOLDER: build # The folder the action should deploy. 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | # Package.pins 40 | # Package.resolved 41 | .build/ 42 | 43 | # CocoaPods 44 | # 45 | # We recommend against adding the Pods directory to your .gitignore. However 46 | # you should judge for yourself, the pros and cons are mentioned at: 47 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 48 | # 49 | # Pods/ 50 | 51 | # Carthage 52 | # 53 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 54 | # Carthage/Checkouts 55 | 56 | Carthage/Build 57 | 58 | # fastlane 59 | # 60 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 61 | # screenshots whenever they are needed. 62 | # For more information about the recommended setup visit: 63 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 64 | 65 | fastlane/report.xml 66 | fastlane/Preview.html 67 | fastlane/screenshots/**/*.png 68 | fastlane/test_output 69 | 70 | output/ 71 | 72 | # utilities for link checking 73 | node_modules 74 | package-lock.json 75 | 76 | # macOS kruft 77 | .DS_Store 78 | 79 | marbles/.venv 80 | marbles/*.svg 81 | -------------------------------------------------------------------------------- /Assets/Images/Combine Illustrations.graffle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heckj/swiftui-notes/e7884cce9b472b143a26a0b755e0f5ebc5b4f9dc/Assets/Images/Combine Illustrations.graffle -------------------------------------------------------------------------------- /Assets/Images/UsingCombineCover.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heckj/swiftui-notes/e7884cce9b472b143a26a0b755e0f5ebc5b4f9dc/Assets/Images/UsingCombineCover.sketch -------------------------------------------------------------------------------- /Assets/Images/UsingCombineWithSwiftGitHubSocial.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heckj/swiftui-notes/e7884cce9b472b143a26a0b755e0f5ebc5b4f9dc/Assets/Images/UsingCombineWithSwiftGitHubSocial.png -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at heckj@mac.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | This repository hosts the content for with code 4 | samples, playgrounds, projects, and notes on using all of the above. It is intended as a 5 | learning resource, and intentionally open to all. 6 | 7 | If you'd like to see something added to the site, projects, or playgrounds then please 8 | feel free to [fork this repository](https://github.com/heckj/swiftui-notes), 9 | [send pull requests](https://github.com/heckj/swiftui-notes/compare?expand=1) with 10 | content updates or fixes, or [open an issue](https://github.com/heckj/swiftui-notes/issues/new/choose) 11 | against the content. 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-2021 Joseph Heck 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /SwiftUI-Notes.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /SwiftUI-Notes.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /SwiftUI-Notes.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "combine-schedulers", 6 | "repositoryURL": "https://github.com/pointfreeco/combine-schedulers", 7 | "state": { 8 | "branch": null, 9 | "revision": "c37e5ae8012fb654af776cc556ff8ae64398c841", 10 | "version": "0.5.0" 11 | } 12 | }, 13 | { 14 | "package": "Entwine", 15 | "repositoryURL": "https://github.com/tcldr/Entwine.git", 16 | "state": { 17 | "branch": "master", 18 | "revision": "4ae1aa1a7bf904b7fdaf4e0cb34da00887f60958", 19 | "version": null 20 | } 21 | }, 22 | { 23 | "package": "xctest-dynamic-overlay", 24 | "repositoryURL": "https://github.com/pointfreeco/xctest-dynamic-overlay", 25 | "state": { 26 | "branch": null, 27 | "revision": "603974e3909ad4b48ba04aad7e0ceee4f077a518", 28 | "version": "0.1.0" 29 | } 30 | } 31 | ] 32 | }, 33 | "version": 1 34 | } 35 | -------------------------------------------------------------------------------- /SwiftUI-Notes.xcodeproj/xcshareddata/xcschemes/SwiftUI-Notes.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 65 | 71 | 72 | 73 | 74 | 80 | 82 | 88 | 89 | 90 | 91 | 93 | 94 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /SwiftUI-Notes.xcodeproj/xcshareddata/xcschemes/UIKit-Combine.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 32 | 33 | 35 | 41 | 42 | 43 | 44 | 45 | 56 | 58 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /SwiftUI-Notes/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // SwiftUI-Notes 4 | // 5 | // Created by Joseph Heck on 6/12/19. 6 | // Copyright © 2019 SwiftUI-Notes. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 14 | // Override point for customization after application launch. 15 | return true 16 | } 17 | 18 | func applicationWillTerminate(_: UIApplication) { 19 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 20 | } 21 | 22 | // MARK: UISceneSession Lifecycle 23 | 24 | func application(_: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options _: UIScene.ConnectionOptions) -> UISceneConfiguration { 25 | // Called when a new scene session is being created. 26 | // Use this method to select a configuration to create the new scene with. 27 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 28 | } 29 | 30 | func application(_: UIApplication, didDiscardSceneSessions _: Set) { 31 | // Called when the user discards a scene session. 32 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 33 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /SwiftUI-Notes/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /SwiftUI-Notes/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /SwiftUI-Notes/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /SwiftUI-Notes/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // SwiftUI-Notes 4 | // 5 | // Created by Joseph Heck on 6/12/19. 6 | // Copyright © 2019 SwiftUI-Notes. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | /// the sample ContentView 12 | struct ContentView: View { 13 | @ObservedObject var model: ReactiveFormModel 14 | 15 | var body: some View { 16 | TabView { 17 | ReactiveForm(model: model) 18 | .tabItem { 19 | Image(systemName: "1.circle") 20 | Text("Reactive Form") 21 | } 22 | 23 | HeadingView(locationModel: LocationProxy()) 24 | .tabItem { 25 | Image(systemName: "mappin.circle") 26 | Text("Location") 27 | } 28 | } 29 | } 30 | } 31 | 32 | // MARK: - SwiftUI VIEW DEBUG 33 | 34 | #if DEBUG 35 | var blah = ReactiveFormModel() 36 | 37 | struct ContentView_Previews: PreviewProvider { 38 | static var previews: some View { 39 | ContentView(model: blah) 40 | } 41 | } 42 | #endif 43 | -------------------------------------------------------------------------------- /SwiftUI-Notes/HeadingView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HeadingView.swift 3 | // SwiftUI-Notes 4 | // 5 | // Created by Joseph Heck on 2/18/20. 6 | // Copyright © 2020 SwiftUI-Notes. All rights reserved. 7 | // 8 | 9 | import CoreLocation 10 | import SwiftUI 11 | 12 | struct HeadingView: View { 13 | @ObservedObject var locationModel: LocationProxy 14 | @State var lastHeading: CLHeading? 15 | @State var lastLocation: CLLocation? 16 | 17 | var body: some View { 18 | VStack { 19 | HStack { 20 | Text("authorization status:") 21 | Text(locationModel.authorizationStatusString()) 22 | } 23 | if locationModel.authorizationStatus == .notDetermined { 24 | Button(action: { 25 | self.locationModel.requestAuthorization() 26 | }) { 27 | Image(systemName: "lock.shield") 28 | Text("Request location authorization") 29 | } 30 | .padding() 31 | .background(RoundedRectangle(cornerRadius: 10).stroke(Color.blue, lineWidth: 1) 32 | ) 33 | } 34 | if self.lastHeading != nil { 35 | Text("Heading: ") + Text(String(self.lastHeading!.description)) 36 | } 37 | if self.lastLocation != nil { 38 | Text("Location: ") + Text(lastLocation!.description) 39 | ZStack { 40 | Circle() 41 | .stroke(Color.blue, lineWidth: 1) 42 | 43 | GeometryReader { geometry in 44 | Path { path in 45 | let minWidthHeight = min(geometry.size.height, geometry.size.width) 46 | 47 | path.move(to: CGPoint(x: geometry.size.width / 2, y: geometry.size.height / 2)) 48 | path.addLine(to: CGPoint(x: geometry.size.width / 2, y: geometry.size.height / 2 - minWidthHeight / 2 + 5)) 49 | } 50 | .stroke() 51 | .rotation(Angle(degrees: self.lastLocation!.course)) 52 | .animation(.linear) 53 | } 54 | } 55 | } 56 | } 57 | .onReceive(self.locationModel.headingPublisher) { heading in 58 | self.lastHeading = heading 59 | } 60 | .onReceive(self.locationModel.locationPublisher, perform: { 61 | self.lastLocation = $0 62 | }) 63 | } 64 | } 65 | 66 | // MARK: - SwiftUI VIEW DEBUG 67 | 68 | #if DEBUG 69 | var locproxy = LocationProxy() 70 | 71 | struct HeadingView_Previews: PreviewProvider { 72 | static var previews: some View { 73 | HeadingView(locationModel: locproxy) 74 | } 75 | } 76 | #endif 77 | -------------------------------------------------------------------------------- /SwiftUI-Notes/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSApplicationCategoryType 22 | public.app-category.utilities 23 | LSRequiresIPhoneOS 24 | 25 | NSAppTransportSecurity 26 | 27 | NSAllowsArbitraryLoads 28 | 29 | 30 | NSLocationUsageDescription 31 | heading for example SwiftUI code 32 | NSLocationWhenInUseUsageDescription 33 | heading for example SwiftUI code 34 | UIApplicationSceneManifest 35 | 36 | UIApplicationSupportsMultipleScenes 37 | 38 | UISceneConfigurations 39 | 40 | UIWindowSceneSessionRoleApplication 41 | 42 | 43 | UILaunchStoryboardName 44 | LaunchScreen 45 | UISceneConfigurationName 46 | Default Configuration 47 | UISceneDelegateClassName 48 | $(PRODUCT_MODULE_NAME).SceneDelegate 49 | 50 | 51 | 52 | 53 | UILaunchStoryboardName 54 | LaunchScreen 55 | UIRequiredDeviceCapabilities 56 | 57 | armv7 58 | 59 | UISupportedInterfaceOrientations 60 | 61 | UIInterfaceOrientationPortrait 62 | UIInterfaceOrientationLandscapeLeft 63 | UIInterfaceOrientationLandscapeRight 64 | 65 | UISupportedInterfaceOrientations~ipad 66 | 67 | UIInterfaceOrientationPortrait 68 | UIInterfaceOrientationPortraitUpsideDown 69 | UIInterfaceOrientationLandscapeLeft 70 | UIInterfaceOrientationLandscapeRight 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /SwiftUI-Notes/LocationModelProxy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LocationModelProxy.swift 3 | // SwiftUI-Notes 4 | // 5 | // Created by Joseph Heck on 2/18/20. 6 | // Copyright © 2020 SwiftUI-Notes. All rights reserved. 7 | // 8 | 9 | import Combine 10 | import CoreLocation 11 | import Foundation 12 | 13 | final class LocationProxy: NSObject, CLLocationManagerDelegate, ObservableObject { 14 | let mgr: CLLocationManager 15 | private let headingSubject: PassthroughSubject 16 | private let locationSubject: PassthroughSubject 17 | var headingPublisher: AnyPublisher 18 | var locationPublisher: AnyPublisher 19 | 20 | @Published var authorizationStatus: CLAuthorizationStatus? 21 | @Published var active = false 22 | 23 | func requestAuthorization() { 24 | mgr.requestWhenInUseAuthorization() 25 | } 26 | 27 | func authorizationStatusString() -> String { 28 | switch authorizationStatus { 29 | case .authorizedWhenInUse: 30 | return "Allowed When In Use" 31 | case .notDetermined: 32 | return "Not Determined" 33 | case .restricted: 34 | return "Restricted" 35 | case .denied: 36 | return "Denied" 37 | case .authorizedAlways: 38 | return "Authorized Always" 39 | case .none: 40 | return "unknown" 41 | @unknown default: 42 | return "unknown" 43 | } 44 | } 45 | 46 | override init() { 47 | mgr = CLLocationManager() 48 | headingSubject = PassthroughSubject() 49 | locationSubject = PassthroughSubject() 50 | headingPublisher = headingSubject.eraseToAnyPublisher() 51 | locationPublisher = locationSubject.eraseToAnyPublisher() 52 | 53 | super.init() 54 | mgr.delegate = self 55 | if #available(iOS 14, *) { 56 | // Use iOS 14 APIs, which guarantees that an initial state will be 57 | // called onto the delegate asserting the current location management 58 | // status, so the overall flow of data be activated from there. 59 | } else { 60 | // if < iOS 14, the CLLocationManager isn't guaranteed to give us an initial 61 | // callback if everything is kosher, so explicitly check it. 62 | authorizationStatus = CLLocationManager.authorizationStatus() 63 | if authorizationStatus == .authorizedAlways || authorizationStatus == .authorizedWhenInUse { 64 | enableEventForwarding() 65 | } 66 | } 67 | } 68 | 69 | func enableEventForwarding() { 70 | if CLLocationManager.headingAvailable() { 71 | mgr.startUpdatingHeading() 72 | } 73 | mgr.startUpdatingLocation() 74 | active = true 75 | } 76 | 77 | func disableEventForwarding() { 78 | mgr.stopUpdatingHeading() 79 | mgr.stopUpdatingLocation() 80 | active = false 81 | } 82 | 83 | // MARK: - delegate methods 84 | 85 | // delegate method from CLLocationManagerDelegate - updates on authorization status changes 86 | func locationManager(_: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) { 87 | authorizationStatus = status 88 | if status == .authorizedAlways || status == .authorizedWhenInUse { 89 | enableEventForwarding() 90 | } else { 91 | disableEventForwarding() 92 | } 93 | } 94 | 95 | /* 96 | * locationManager:didUpdateHeading: 97 | * 98 | * Discussion: 99 | * Invoked when a new heading is available. 100 | */ 101 | func locationManager(_: CLLocationManager, didUpdateHeading newHeading: CLHeading) { 102 | // NOTE(heckj): simulator will *NOT* trigger this value, but it will send location updates 103 | // print(newHeading) 104 | headingSubject.send(newHeading) 105 | } 106 | 107 | func locationManager(_: CLLocationManager, didUpdateLocations locations: [CLLocation]) { 108 | // print(locations) 109 | for loc in locations { 110 | locationSubject.send(loc) 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /SwiftUI-Notes/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /SwiftUI-Notes/PublisherView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PublisherView.swift 3 | // SwiftUI-Notes 4 | // 5 | // Created by Joseph Heck on 2/7/21. 6 | // Copyright © 2021 SwiftUI-Notes. All rights reserved. 7 | // 8 | 9 | import Combine 10 | import SwiftUI 11 | 12 | struct PublisherBindingExampleView: View { 13 | @State private var filterText = "" 14 | @State private var delayed = "" 15 | 16 | private var relay = PassthroughSubject() 17 | private var debouncedPublisher: AnyPublisher 18 | 19 | init() { 20 | debouncedPublisher = relay 21 | .debounce(for: 1, scheduler: RunLoop.main) 22 | .eraseToAnyPublisher() 23 | } 24 | 25 | var body: some View { 26 | VStack { 27 | TextField("filter", text: $filterText) 28 | .onChange(of: filterText, perform: { value in 29 | relay.send(value) 30 | }) 31 | Text("Delayed result: \(delayed)") 32 | .onReceive(debouncedPublisher, perform: { value in 33 | delayed = value 34 | }) 35 | } 36 | } 37 | } 38 | 39 | struct PublisherView_Previews: PreviewProvider { 40 | static var previews: some View { 41 | PublisherBindingExampleView() 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /SwiftUI-Notes/ReactiveForm.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReactiveForm.swift 3 | // SwiftUI-Notes 4 | // 5 | // Created by Joseph Heck on 2/5/20. 6 | // Copyright © 2020 SwiftUI-Notes. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct ReactiveForm: View { 12 | @ObservedObject var model: ReactiveFormModel 13 | // $model is a ObservedObject.Wrapper 14 | // and $model.objectWillChange is a Binding 15 | @State private var buttonIsDisabled = true 16 | // $buttonIsDisabled is a Binding 17 | 18 | var body: some View { 19 | VStack { 20 | Text("Reactive Form") 21 | .font(.headline) 22 | 23 | Form { 24 | TextField("first entry", text: $model.firstEntry) 25 | .textFieldStyle(RoundedBorderTextFieldStyle()) 26 | .lineLimit(1) 27 | .multilineTextAlignment(.center) 28 | .padding() 29 | 30 | TextField("second entry", text: $model.secondEntry) 31 | .textFieldStyle(RoundedBorderTextFieldStyle()) 32 | .multilineTextAlignment(.center) 33 | .padding() 34 | 35 | VStack { 36 | ForEach(model.validationMessages, id: \.self) { msg in 37 | Text(msg) 38 | .foregroundColor(.red) 39 | .font(.callout) 40 | } 41 | } 42 | } 43 | 44 | Button(action: {}) { 45 | Text("Submit") 46 | }.disabled(buttonIsDisabled) 47 | .onReceive(model.submitAllowed) { submitAllowed in 48 | self.buttonIsDisabled = !submitAllowed 49 | } 50 | .padding() 51 | .background(RoundedRectangle(cornerRadius: 10).stroke(Color.blue, lineWidth: 1) 52 | ) 53 | 54 | Spacer() 55 | } 56 | } 57 | } 58 | 59 | // MARK: - SwiftUI VIEW DEBUG 60 | 61 | #if DEBUG 62 | var localModel = ReactiveFormModel() 63 | 64 | struct ReactiveForm_Previews: PreviewProvider { 65 | static var previews: some View { 66 | ReactiveForm(model: localModel) 67 | } 68 | } 69 | #endif 70 | -------------------------------------------------------------------------------- /SwiftUI-Notes/ReactiveFormModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReactiveFormModel.swift 3 | // SwiftUI-Notes 4 | // 5 | // Created by Joseph Heck on 2/5/20. 6 | // Copyright © 2020 SwiftUI-Notes. All rights reserved. 7 | // 8 | 9 | import Combine 10 | import Foundation 11 | 12 | class ReactiveFormModel: ObservableObject { 13 | @Published var firstEntry: String = "" 14 | @Published var secondEntry: String = "" 15 | @Published var validationMessages = [String]() 16 | 17 | private var cancellableSet: Set = [] 18 | 19 | var submitAllowed: AnyPublisher! 20 | 21 | init() { 22 | let validationPipeline = Publishers.CombineLatest($firstEntry, $secondEntry) 23 | .map { arg -> [String] in 24 | var diagMsgs = [String]() 25 | let (value, value_repeat) = arg 26 | if !(value_repeat == value) { 27 | diagMsgs.append("Values for fields must match.") 28 | } 29 | if value.count < 5 || value_repeat.count < 5 { 30 | diagMsgs.append("Please enter values of at least 5 characters.") 31 | } 32 | return diagMsgs 33 | } 34 | .share() 35 | 36 | submitAllowed = validationPipeline 37 | .map { stringArray in 38 | stringArray.count < 1 39 | } 40 | .eraseToAnyPublisher() 41 | 42 | _ = validationPipeline 43 | .assign(to: \.validationMessages, on: self) 44 | .store(in: &cancellableSet) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /SwiftUI-Notes/SampleView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SampleView.swift 3 | // SwiftUI-Notes 4 | // 5 | // Created by Joseph Heck on 2/5/20. 6 | // Copyright © 2020 SwiftUI-Notes. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct SampleView: View { 12 | var body: some View { 13 | VStack { 14 | Spacer() 15 | Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) 16 | Spacer() 17 | Text("Another bit") 18 | Spacer() 19 | } 20 | } 21 | } 22 | 23 | struct SampleView_Previews: PreviewProvider { 24 | static var previews: some View { 25 | SampleView() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /SwiftUI-Notes/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // SwiftUI-Notes 4 | // 5 | // Created by Joseph Heck on 6/12/19. 6 | // Copyright © 2019 SwiftUI-Notes. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import UIKit 11 | 12 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 13 | var window: UIWindow? 14 | 15 | func scene(_ scene: UIScene, willConnectTo _: UISceneSession, options _: UIScene.ConnectionOptions) { 16 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 17 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 18 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 19 | 20 | // Create the model that backs the data in our view 21 | let model = ReactiveFormModel() 22 | 23 | // Create the SwiftUI view that provides the window contents. 24 | let contentView = ContentView(model: model) 25 | 26 | // Use a UIHostingController as window root view controller. 27 | if let windowScene = scene as? UIWindowScene { 28 | let window = UIWindow(windowScene: windowScene) 29 | window.rootViewController = UIHostingController(rootView: contentView) 30 | self.window = window 31 | window.makeKeyAndVisible() 32 | } 33 | } 34 | 35 | func sceneDidDisconnect(_: UIScene) { 36 | // Called as the scene is being released by the system. 37 | // This occurs shortly after the scene enters the background, or when its session is discarded. 38 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 39 | // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). 40 | } 41 | 42 | func sceneDidBecomeActive(_: UIScene) { 43 | // Called when the scene has moved from an inactive state to an active state. 44 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 45 | } 46 | 47 | func sceneWillResignActive(_: UIScene) { 48 | // Called when the scene will move from an active state to an inactive state. 49 | // This may occur due to temporary interruptions (ex. an incoming phone call). 50 | } 51 | 52 | func sceneWillEnterForeground(_: UIScene) { 53 | // Called as the scene transitions from the background to the foreground. 54 | // Use this method to undo the changes made on entering the background. 55 | } 56 | 57 | func sceneDidEnterBackground(_: UIScene) { 58 | // Called as the scene transitions from the foreground to the background. 59 | // Use this method to save data, release shared resources, and store enough scene-specific state information 60 | // to restore the scene back to its current state. 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /SwiftUI-Notes/SwiftUI-Notes.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.network.client 8 | 9 | com.apple.security.personal-information.location 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /SwiftUI-Notes/SwiftUITabView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftUITabView.swift 3 | // SwiftUI-Notes 4 | // 5 | // Created by Joseph Heck on 2/5/20. 6 | // Copyright © 2020 SwiftUI-Notes. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct SwiftUITabView: View { 12 | var body: some View { 13 | TabView { 14 | SampleView() 15 | .tabItem { 16 | Image(systemName: "1.circle") 17 | Text("Reactive Form") 18 | } 19 | Text("Second Tab") 20 | .tabItem { 21 | Image(systemName: "2.square.fill") 22 | Text("Dos") 23 | } 24 | } 25 | .font(.headline) 26 | } 27 | } 28 | 29 | struct SwiftUITabView_Previews: PreviewProvider { 30 | static var previews: some View { 31 | SwiftUITabView() 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /SwiftUI-NotesTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /SwiftUI-NotesTests/SwiftUI_NotesTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftUI_NotesTests.swift 3 | // SwiftUI-NotesTests 4 | // 5 | // Created by Joseph Heck on 6/12/19. 6 | // Copyright © 2019 SwiftUI-Notes. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | class SwiftUI_NotesTests: XCTestCase { 12 | override func setUp() { 13 | // Put setup code here. This method is called before the invocation of each test method in the class. 14 | } 15 | 16 | override func tearDown() { 17 | // Put teardown code here. This method is called after the invocation of each test method in the class. 18 | } 19 | 20 | func testExample() { 21 | // This is an example of a functional test case. 22 | // Use XCTAssert and related functions to verify your tests produce the correct results. 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /SwiftUI_IOS_Playground.playground/Contents.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import PlaygroundSupport 3 | import SwiftUI 4 | 5 | struct MyView: View { 6 | var body: some View { 7 | Text("Hello, world!") 8 | } 9 | } 10 | 11 | let vc = UIHostingController(rootView: MyView()) 12 | 13 | let foo = Publishers.Sequence<[String], Never>(sequence: ["foo", "bar", "baz"]) 14 | // this publishes the stream combo: , 15 | 16 | let reader = foo.sink { data in 17 | print(data) 18 | } 19 | 20 | // Present the view controller in the Live View window 21 | PlaygroundPage.current.liveView = vc 22 | -------------------------------------------------------------------------------- /SwiftUI_IOS_Playground.playground/contents.xcplayground: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /UIKit-Combine/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // UIKit-Combine 4 | // 5 | // Created by Joseph Heck on 7/7/19. 6 | // Copyright © 2019 SwiftUI-Notes. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 14 | // Override point for customization after application launch. 15 | return true 16 | } 17 | 18 | // MARK: UISceneSession Lifecycle 19 | 20 | func application(_: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options _: UIScene.ConnectionOptions) -> UISceneConfiguration { 21 | // Called when a new scene session is being created. 22 | // Use this method to select a configuration to create the new scene with. 23 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 24 | } 25 | 26 | func application(_: UIApplication, didDiscardSceneSessions _: Set) { 27 | // Called when the user discards a scene session. 28 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 29 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /UIKit-Combine/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /UIKit-Combine/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /UIKit-Combine/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /UIKit-Combine/FormViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FormViewController.swift 3 | // UIKit-Combine 4 | // 5 | // Created by Joseph Heck on 7/19/19. 6 | // Copyright © 2019 SwiftUI-Notes. All rights reserved. 7 | // 8 | 9 | import Combine 10 | import UIKit 11 | 12 | class FormViewController: UIViewController { 13 | @IBOutlet var value1_input: UITextField! 14 | @IBOutlet var value2_input: UITextField! 15 | @IBOutlet var value2_repeat_input: UITextField! 16 | @IBOutlet var submission_button: UIButton! 17 | @IBOutlet var value1_message_label: UILabel! 18 | @IBOutlet var value2_message_label: UILabel! 19 | 20 | @IBAction func value1_updated(_ sender: UITextField) { 21 | value1 = sender.text ?? "" 22 | } 23 | 24 | @IBAction func value2_updated(_ sender: UITextField) { 25 | value2 = sender.text ?? "" 26 | } 27 | 28 | @IBAction func value2_repeat_updated(_ sender: UITextField) { 29 | value2_repeat = sender.text ?? "" 30 | } 31 | 32 | @Published var value1: String = "" 33 | @Published var value2: String = "" 34 | @Published var value2_repeat: String = "" 35 | 36 | var validatedValue1: AnyPublisher { 37 | return $value1.map { value1 in 38 | guard value1.count > 2 else { 39 | DispatchQueue.main.async { 40 | self.value1_message_label.text = "minimum of 3 characters required" 41 | } 42 | return nil 43 | } 44 | DispatchQueue.main.async { 45 | self.value1_message_label.text = "" 46 | } 47 | return value1 48 | }.eraseToAnyPublisher() 49 | } 50 | 51 | var validatedValue2: AnyPublisher { 52 | return Publishers.CombineLatest($value2, $value2_repeat) 53 | .receive(on: RunLoop.main) 54 | .map { value2, value2_repeat in 55 | guard value2_repeat == value2, value2.count > 4 else { 56 | self.value2_message_label.text = "values must match and have at least 5 characters" 57 | return nil 58 | } 59 | self.value2_message_label.text = "" 60 | return value2 61 | }.eraseToAnyPublisher() 62 | } 63 | 64 | var readyToSubmit: AnyPublisher<(String, String)?, Never> { 65 | return Publishers.CombineLatest(validatedValue2, validatedValue1) 66 | .map { value2, value1 in 67 | guard let realValue2 = value2, let realValue1 = value1 else { 68 | return nil 69 | } 70 | return (realValue2, realValue1) 71 | } 72 | .eraseToAnyPublisher() 73 | } 74 | 75 | private var cancellableSet: Set = [] 76 | 77 | override func viewDidLoad() { 78 | super.viewDidLoad() 79 | 80 | readyToSubmit 81 | .map { $0 != nil } 82 | .receive(on: RunLoop.main) 83 | .assign(to: \.isEnabled, on: submission_button) 84 | .store(in: &cancellableSet) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /UIKit-Combine/GithubAPI.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GithubAPI.swift 3 | // UIKit-Combine 4 | // 5 | // Created by Joseph Heck on 7/13/19. 6 | // Copyright © 2019 SwiftUI-Notes. All rights reserved. 7 | // 8 | 9 | import Combine 10 | import Foundation 11 | 12 | enum APIFailureCondition: Error { 13 | case invalidServerResponse 14 | } 15 | 16 | struct GithubAPIUser: Decodable { 17 | // A very *small* subset of the content available about 18 | // a github API user for example: 19 | // https://api.github.com/users/heckj 20 | let login: String 21 | let public_repos: Int 22 | let avatar_url: String 23 | } 24 | 25 | enum GithubAPI { 26 | // NOTE(heckj): I've also seen this kind of API access 27 | // object set up with with a class and static methods on the class. 28 | // I don't know that there's a specific benefit to make this a value 29 | // type/struct with a function on it. 30 | 31 | /// externally accessible publsher that indicates that network activity is happening in the API proxy 32 | static let networkActivityPublisher = PassthroughSubject() 33 | 34 | /// creates a one-shot publisher that provides a GithubAPI User 35 | /// object as the end result. This method was specifically designed to 36 | /// return a list of 1 object, as opposed to the object itself to make 37 | /// it easier to distinguish a "no user" result (empty list) 38 | /// representation that could be dealt with more easily in a Combine 39 | /// pipeline than an optional value. The expected return types is a 40 | /// Publisher that returns either an empty list, or a list of one 41 | /// GithubAPUser, and with a failure return type of Never, so it's 42 | /// suitable for recurring pipeline updates working with a @Published 43 | /// data source. 44 | /// - Parameter username: username to be retrieved from the Github API 45 | static func retrieveGithubUser(username: String) -> AnyPublisher<[GithubAPIUser], Never> { 46 | if username.count < 3 { 47 | return Just([]).eraseToAnyPublisher() 48 | // return Publishers.Empty() 49 | // .eraseToAnyPublisher() 50 | } 51 | let assembledURL = String("https://api.github.com/users/\(username)") 52 | let publisher = URLSession.shared.dataTaskPublisher(for: URL(string: assembledURL)!) 53 | .handleEvents(receiveSubscription: { _ in 54 | networkActivityPublisher.send(true) 55 | }, receiveCompletion: { _ in 56 | networkActivityPublisher.send(false) 57 | }, receiveCancel: { 58 | networkActivityPublisher.send(false) 59 | }) 60 | .tryMap { data, response -> Data in 61 | guard let httpResponse = response as? HTTPURLResponse, 62 | httpResponse.statusCode == 200 63 | else { 64 | throw APIFailureCondition.invalidServerResponse 65 | } 66 | return data 67 | } 68 | .decode(type: GithubAPIUser.self, decoder: JSONDecoder()) 69 | .map { 70 | [$0] 71 | } 72 | .replaceError(with: []) 73 | // ^^ when I originally wrote this method, I was returning 74 | // a GithubAPIUser? optional, and then a GithubAPIUser without 75 | // optional. I ended up converting this to return an empty 76 | // list as the "error output replacement" so that I could 77 | // represent that the current value requested didn't *have* a 78 | // correct github API response. 79 | .eraseToAnyPublisher() 80 | return publisher 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /UIKit-Combine/HeadingViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HeadingViewController.swift 3 | // UIKit-Combine 4 | // 5 | // Created by Joseph Heck on 7/15/19. 6 | // Copyright © 2019 SwiftUI-Notes. All rights reserved. 7 | // 8 | 9 | import Combine 10 | import CoreLocation 11 | import UIKit 12 | 13 | class HeadingViewController: UIViewController { 14 | var headingSubscriber: AnyCancellable? 15 | 16 | let coreLocationProxy = LocationHeadingProxy() 17 | var headingBackgroundQueue: DispatchQueue = .init(label: "headingBackgroundQueue") 18 | 19 | // MARK: - lifecycle methods 20 | 21 | @IBOutlet var permissionButton: UIButton! 22 | @IBOutlet var activateTrackingSwitch: UISwitch! 23 | @IBOutlet var headingLabel: UILabel! 24 | @IBOutlet var locationPermissionLabel: UILabel! 25 | 26 | @IBAction func requestPermission(_: UIButton) { 27 | print("requesting corelocation permission") 28 | _ = Future { promise in 29 | self.coreLocationProxy.mgr.requestWhenInUseAuthorization() 30 | return promise(.success(1)) 31 | } 32 | .delay(for: 2.0, scheduler: headingBackgroundQueue) 33 | .receive(on: RunLoop.main) 34 | .sink { _ in 35 | print("updating corelocation permission label") 36 | self.updatePermissionStatus() 37 | } 38 | } 39 | 40 | @IBAction func trackingToggled(_ sender: UISwitch) { 41 | switch sender.isOn { 42 | case true: 43 | coreLocationProxy.enable() 44 | print("Enabling heading tracking") 45 | case false: 46 | coreLocationProxy.disable() 47 | print("Disabling heading tracking") 48 | } 49 | } 50 | 51 | func updatePermissionStatus() { 52 | // When originally written (for iOS 13), this method was available 53 | // for requesting current status at any time. With iOS 14, that's no 54 | // longer the case and it shows as deprecated, with the expected path 55 | // to get this information being from a CoreLocationManager Delegate 56 | // callback. 57 | let x = CLLocationManager.authorizationStatus() 58 | switch x { 59 | case .authorizedWhenInUse: 60 | locationPermissionLabel.text = "Allowed when in use" 61 | case .notDetermined: 62 | locationPermissionLabel.text = "notDetermined" 63 | case .restricted: 64 | locationPermissionLabel.text = "restricted" 65 | case .denied: 66 | locationPermissionLabel.text = "denied" 67 | case .authorizedAlways: 68 | locationPermissionLabel.text = "authorizedAlways" 69 | @unknown default: 70 | locationPermissionLabel.text = "unknown default" 71 | } 72 | } 73 | 74 | override func viewDidLoad() { 75 | super.viewDidLoad() 76 | // Do any additional setup after loading the view. 77 | 78 | // request authorization for the corelocation data 79 | updatePermissionStatus() 80 | 81 | let corelocationsub = coreLocationProxy 82 | .publisher 83 | .print("headingSubscriber") 84 | .receive(on: RunLoop.main) 85 | .sink(receiveCompletion: { _ in }, 86 | receiveValue: { someValue in 87 | self.headingLabel.text = String(someValue.trueHeading) 88 | }) 89 | 90 | headingSubscriber = AnyCancellable(corelocationsub) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /UIKit-Combine/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | UISceneConfigurations 28 | 29 | UIWindowSceneSessionRoleApplication 30 | 31 | 32 | UILaunchStoryboardName 33 | LaunchScreen 34 | UISceneConfigurationName 35 | Default Configuration 36 | UISceneDelegateClassName 37 | $(PRODUCT_MODULE_NAME).SceneDelegate 38 | UISceneStoryboardFile 39 | Main 40 | 41 | 42 | 43 | 44 | UILaunchStoryboardName 45 | LaunchScreen 46 | UIMainStoryboardFile 47 | Main 48 | UIRequiredDeviceCapabilities 49 | 50 | UISupportedInterfaceOrientations 51 | 52 | UIInterfaceOrientationPortrait 53 | UIInterfaceOrientationLandscapeLeft 54 | UIInterfaceOrientationLandscapeRight 55 | 56 | NSLocationUsageDescription 57 | example code 58 | NSLocationWhenInUseUsageDescription 59 | heading for combine example 60 | UISupportedInterfaceOrientations~ipad 61 | 62 | UIInterfaceOrientationPortrait 63 | UIInterfaceOrientationPortraitUpsideDown 64 | UIInterfaceOrientationLandscapeLeft 65 | UIInterfaceOrientationLandscapeRight 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /UIKit-Combine/LocationHeadingProxy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LocationHeadingProxy.swift 3 | // UIKit-Combine 4 | // 5 | // Created by Joseph Heck on 7/13/19. 6 | // Copyright © 2019 SwiftUI-Notes. All rights reserved. 7 | // 8 | 9 | import Combine 10 | import CoreLocation 11 | import Foundation 12 | 13 | final class LocationHeadingProxy: NSObject, CLLocationManagerDelegate { 14 | let mgr: CLLocationManager 15 | private let headingPublisher: PassthroughSubject 16 | var publisher: AnyPublisher 17 | 18 | override init() { 19 | mgr = CLLocationManager() 20 | headingPublisher = PassthroughSubject() 21 | publisher = headingPublisher.eraseToAnyPublisher() 22 | 23 | super.init() 24 | mgr.delegate = self 25 | } 26 | 27 | func enable() { 28 | mgr.startUpdatingHeading() 29 | } 30 | 31 | func disable() { 32 | mgr.stopUpdatingHeading() 33 | } 34 | 35 | // MARK: - delegate methods 36 | 37 | /* 38 | * locationManager:didUpdateHeading: 39 | * 40 | * Discussion: 41 | * Invoked when a new heading is available. 42 | */ 43 | func locationManager(_: CLLocationManager, didUpdateHeading newHeading: CLHeading) { 44 | headingPublisher.send(newHeading) 45 | } 46 | 47 | /* 48 | * locationManager:didFailWithError: 49 | * Discussion: 50 | * Invoked when an error has occurred. Error types are defined in "CLError.h". 51 | */ 52 | func locationManager(_: CLLocationManager, didFailWithError error: Error) { 53 | headingPublisher.send(completion: Subscribers.Completion.failure(error)) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /UIKit-Combine/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // UIKit-Combine 4 | // 5 | // Created by Joseph Heck on 7/7/19. 6 | // Copyright © 2019 SwiftUI-Notes. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 12 | var window: UIWindow? 13 | 14 | func scene(_ scene: UIScene, willConnectTo _: UISceneSession, options _: UIScene.ConnectionOptions) { 15 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 16 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 17 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 18 | guard let _ = (scene as? UIWindowScene) else { return } 19 | } 20 | 21 | func sceneDidDisconnect(_: UIScene) { 22 | // Called as the scene is being released by the system. 23 | // This occurs shortly after the scene enters the background, or when its session is discarded. 24 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 25 | // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). 26 | } 27 | 28 | func sceneDidBecomeActive(_: UIScene) { 29 | // Called when the scene has moved from an inactive state to an active state. 30 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 31 | } 32 | 33 | func sceneWillResignActive(_: UIScene) { 34 | // Called when the scene will move from an active state to an inactive state. 35 | // This may occur due to temporary interruptions (ex. an incoming phone call). 36 | } 37 | 38 | func sceneWillEnterForeground(_: UIScene) { 39 | // Called as the scene transitions from the background to the foreground. 40 | // Use this method to undo the changes made on entering the background. 41 | } 42 | 43 | func sceneDidEnterBackground(_: UIScene) { 44 | // Called as the scene transitions from the foreground to the background. 45 | // Use this method to save data, release shared resources, and store enough scene-specific state information 46 | // to restore the scene back to its current state. 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /UIKit-CombineTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /UIKit-CombineTests/UIKit_CombineTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIKit_CombineTests.swift 3 | // UIKit-CombineTests 4 | // 5 | // Created by Joseph Heck on 7/7/19. 6 | // Copyright © 2019 SwiftUI-Notes. All rights reserved. 7 | // 8 | 9 | @testable import UIKit_Combine 10 | import XCTest 11 | 12 | class UIKit_CombineTests: XCTestCase { 13 | override func setUp() { 14 | // Put setup code here. This method is called before the invocation of each test method in the class. 15 | } 16 | 17 | override func tearDown() { 18 | // Put teardown code here. This method is called after the invocation of each test method in the class. 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /UsingCombineTests/BreakpointPublisherTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BreakpointPublisherTests.swift 3 | // UsingCombineTests 4 | // 5 | // Created by Joseph Heck on 7/27/19. 6 | // Copyright © 2019 SwiftUI-Notes. All rights reserved. 7 | // 8 | 9 | import Combine 10 | import XCTest 11 | 12 | class BreakpointPublisherTests: XCTestCase { 13 | enum TestFailureCondition: Error { 14 | case invalidServerResponse 15 | } 16 | 17 | /* NOTE(heckj): 18 | - these tests have all been prefixed with SKIP_ so they won't be run automatically with a whole 19 | project validation. They explicitly drop breakpoints into the debugger, which is great when 20 | you're actively debugging, but a complete PITA when you're trying to see a whole test sequence run. 21 | */ 22 | func SKIP_testBreakpointOnError() { 23 | let publisher = PassthroughSubject() 24 | 25 | // this sets up the chain of whatever it's going to do 26 | let cancellable = publisher 27 | .tryMap { _ in 28 | throw TestFailureCondition.invalidServerResponse 29 | } 30 | .breakpointOnError() 31 | .sink( 32 | // sink captures and terminates the pipeline of operators 33 | receiveCompletion: { completion in 34 | print("sink captured the completion of \(String(describing: completion))") 35 | }, 36 | receiveValue: { aValue in 37 | print("sink captured the result of \(String(describing: aValue))") 38 | } 39 | ) 40 | 41 | publisher.send("DATA IN") 42 | publisher.send(completion: .finished) 43 | XCTAssertNotNil(cancellable) 44 | } 45 | 46 | func SKIP_testBreakpointOnSubscription() { 47 | let publisher = PassthroughSubject() 48 | 49 | // this sets up the chain of whatever it's going to do 50 | let cancellable = publisher 51 | .breakpoint(receiveSubscription: { _ in 52 | true // triggers breakpoint 53 | }, receiveOutput: { _ in 54 | false 55 | }, receiveCompletion: { _ in 56 | false 57 | }) 58 | .sink( 59 | receiveCompletion: { completion in 60 | print("sink captured the completion of \(String(describing: completion))") 61 | }, 62 | receiveValue: { aValue in 63 | print("sink captured the result of \(String(describing: aValue))") 64 | } 65 | ) 66 | 67 | publisher.send("DATA IN") 68 | publisher.send(completion: .finished) 69 | XCTAssertNotNil(cancellable) 70 | } 71 | 72 | func SKIP_testBreakpointOnData() { 73 | let publisher = PassthroughSubject() 74 | let cancellable = publisher 75 | .breakpoint(receiveSubscription: { _ in 76 | false 77 | }, receiveOutput: { _ in 78 | true // triggers breakpoint 79 | }, receiveCompletion: { _ in 80 | false 81 | }) 82 | .map { 83 | $0 // does nothing, but can be convenient to hang a debugger breakpoint on to see the data 84 | } 85 | .sink( 86 | // sink captures and terminates the pipeline of operators 87 | receiveCompletion: { completion in 88 | print("sink captured the completion of \(String(describing: completion))") 89 | }, 90 | receiveValue: { aValue in 91 | print("sink captured the result of \(String(describing: aValue))") 92 | } 93 | ) 94 | 95 | publisher.send("DATA IN") 96 | publisher.send(completion: .finished) 97 | XCTAssertNotNil(cancellable) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /UsingCombineTests/DeferredPublisherTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DeferredPublisherTests.swift 3 | // UsingCombineTests 4 | // 5 | // Created by Joseph Heck on 8/11/19. 6 | // Copyright © 2019 SwiftUI-Notes. All rights reserved. 7 | // 8 | 9 | import Combine 10 | import XCTest 11 | 12 | class DeferredPublisherTests: XCTestCase { 13 | enum TestFailureCondition: Error { 14 | case anErrorExample 15 | } 16 | 17 | // example of a asynchronous function to be called from within a Future and its completion closure 18 | func asyncAPICall(sabotage: Bool, completion completionBlock: @escaping ((Bool, Error?) -> Void)) { 19 | DispatchQueue.global(qos: .background).async { 20 | let delay = Int.random(in: 1 ... 3) 21 | print(" * making async call (delay of \(delay) seconds)") 22 | sleep(UInt32(delay)) 23 | if sabotage { 24 | completionBlock(false, TestFailureCondition.anErrorExample) 25 | } 26 | completionBlock(true, nil) 27 | } 28 | } 29 | 30 | func testDeferredFuturePublisher() { 31 | // setup 32 | var outputValue = false 33 | let expectation = XCTestExpectation(description: debugDescription) 34 | 35 | let deferredPublisher = Deferred { 36 | Future { promise in 37 | self.asyncAPICall(sabotage: false) { grantedAccess, err in 38 | if let err = err { 39 | return promise(.failure(err)) 40 | } 41 | return promise(.success(grantedAccess)) 42 | } 43 | } 44 | }.eraseToAnyPublisher() 45 | 46 | // the creating the future publisher 47 | 48 | // driving it by attaching it to .sink 49 | let cancellable = deferredPublisher.sink(receiveCompletion: { err in 50 | print(".sink() received the completion: ", String(describing: err)) 51 | expectation.fulfill() 52 | }, receiveValue: { value in 53 | print(".sink() received value: ", value) 54 | outputValue = value 55 | }) 56 | 57 | wait(for: [expectation], timeout: 5.0) 58 | XCTAssertTrue(outputValue) 59 | XCTAssertNotNil(cancellable) 60 | } 61 | 62 | func testDeferredPublisher() { 63 | let expectation = XCTestExpectation(description: debugDescription) 64 | 65 | let deferredPublisher = Deferred { 66 | Just("hello") 67 | }.eraseToAnyPublisher() 68 | 69 | // The core of "Deferred" is that the closure that generates the published is not invoked 70 | // until a subscriber is attached, then it creates the publisher "just in time". 71 | // I'm afraid I haven't figured out any sane way to illustrate that with a unit test... 72 | 73 | let cancellable = deferredPublisher 74 | .sink(receiveCompletion: { completion in 75 | print(".sink() received the completion", String(describing: completion)) 76 | switch completion { 77 | case .finished: 78 | break 79 | case let .failure(anError): 80 | XCTFail("No failure should be received from empty") 81 | print("received error: ", anError) 82 | } 83 | expectation.fulfill() 84 | }, receiveValue: { valueReceived in 85 | XCTAssertEqual(valueReceived, "hello") 86 | print(".sink() data received \(valueReceived)") 87 | }) 88 | 89 | wait(for: [expectation], timeout: 1.0) 90 | XCTAssertNotNil(cancellable) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /UsingCombineTests/EmptyPublisherTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EmptyPublisherTests.swift 3 | // UsingCombineTests 4 | // 5 | // Created by Joseph Heck on 7/11/19. 6 | // Copyright © 2019 SwiftUI-Notes. All rights reserved. 7 | // 8 | 9 | import Combine 10 | import XCTest 11 | 12 | class EmptyPublisherTests: XCTestCase { 13 | func testEmptyPublisher() { 14 | let expectation = XCTestExpectation(description: debugDescription) 15 | 16 | let cancellable = Empty() 17 | .sink(receiveCompletion: { completion in 18 | print(".sink() received the completion", String(describing: completion)) 19 | switch completion { 20 | case .finished: 21 | expectation.fulfill() 22 | case let .failure(anError): 23 | print("received error: ", anError) 24 | XCTFail("No failure should be received from empty") 25 | } 26 | }, receiveValue: { postmanResponse in 27 | XCTFail("No vaue should be received from empty") 28 | print(".sink() data received \(postmanResponse)") 29 | }) 30 | 31 | wait(for: [expectation], timeout: 5.0) 32 | XCTAssertNotNil(cancellable) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /UsingCombineTests/EntwineTestExampleTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EntwineTestExampleTests.swift 3 | // UsingCombineTests 4 | // 5 | // Originally from the EntwineTest project README: 6 | // https://github.com/tcldr/Entwine/blob/master/Assets/EntwineTest/README.md 7 | 8 | import Combine 9 | import EntwineTest 10 | // library loaded from https://github.com/tcldr/Entwine/blob/master/Assets/EntwineTest/README.md 11 | // as a swift package https://github.com/tcldr/Entwine.git : 0.6.0, Next Major Version 12 | import XCTest 13 | 14 | class EntwineTestExampleTests: XCTestCase { 15 | func testMap() { 16 | let testScheduler = TestScheduler(initialClock: 0) 17 | 18 | // creates a publisher that will schedule its elements relatively, at the point of subscription 19 | let testablePublisher: TestablePublisher = testScheduler.createRelativeTestablePublisher([ 20 | (100, .input("a")), 21 | (200, .input("b")), 22 | (300, .input("c")), 23 | ]) 24 | 25 | // a publisher that maps strings to uppercase 26 | let subjectUnderTest = testablePublisher.map { $0.uppercased() } 27 | 28 | // uses the method described above (schedules a subscription at 200, to be cancelled at 900) 29 | let results = testScheduler.start { subjectUnderTest } 30 | 31 | XCTAssertEqual(results.recordedOutput, [ 32 | (200, .subscription), // subscribed at 200 33 | (300, .input("A")), // received uppercased input @ 100 + subscription time 34 | (400, .input("B")), // received uppercased input @ 200 + subscription time 35 | (500, .input("C")), // received uppercased input @ 300 + subscription time 36 | ]) 37 | } 38 | 39 | func testExampleUsingVirtualTimeScheduler() { 40 | let scheduler = TestScheduler(initialClock: 0) 41 | var didSink = false 42 | let cancellable = Just(1) 43 | .delay(for: 1, scheduler: scheduler) 44 | .sink { _ in 45 | didSink = true 46 | } 47 | 48 | XCTAssertNotNil(cancellable) 49 | // where a real scheduler would have triggered when .sink() was invoked 50 | // the virtual time scheduler requires resume() to commence and runs to 51 | // completion. 52 | scheduler.resume() 53 | XCTAssertTrue(didSink) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /UsingCombineTests/FailedPublisherTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FailedPublisherTests.swift 3 | // UsingCombineTests 4 | // 5 | // Created by Joseph Heck on 8/11/19. 6 | // Copyright © 2019 SwiftUI-Notes. All rights reserved. 7 | // 8 | 9 | import Combine 10 | import XCTest 11 | 12 | class FailedPublisherTests: XCTestCase { 13 | enum TestFailureCondition: Error { 14 | case exampleFailure 15 | } 16 | 17 | func testFailPublisher() { 18 | let expectation = XCTestExpectation(description: debugDescription) 19 | 20 | let cancellable = Fail(error: TestFailureCondition.exampleFailure) 21 | .sink(receiveCompletion: { completion in 22 | print(".sink() received the completion", String(describing: completion)) 23 | switch completion { 24 | case .finished: 25 | XCTFail("No finished should be received from empty") 26 | case let .failure(anError): 27 | print("received error: ", anError) 28 | } 29 | expectation.fulfill() 30 | }, receiveValue: { responseValue in 31 | XCTFail("No vaue should be received from empty") 32 | print(".sink() data received \(responseValue)") 33 | }) 34 | 35 | wait(for: [expectation], timeout: 1.0) 36 | XCTAssertNotNil(cancellable) 37 | } 38 | 39 | func testFailPublisherAltInitializer() { 40 | let expectation = XCTestExpectation(description: debugDescription) 41 | 42 | let cancellable = Fail(outputType: String.self, failure: TestFailureCondition.exampleFailure) 43 | .sink(receiveCompletion: { completion in 44 | print(".sink() received the completion", String(describing: completion)) 45 | switch completion { 46 | case .finished: 47 | XCTFail("No finished should be received from empty") 48 | case let .failure(anError): 49 | print("received error: ", anError) 50 | } 51 | expectation.fulfill() 52 | }, receiveValue: { responseValue in 53 | XCTFail("No vaue should be received from empty") 54 | print(".sink() data received \(responseValue)") 55 | }) 56 | 57 | wait(for: [expectation], timeout: 1.0) 58 | XCTAssertNotNil(cancellable) 59 | } 60 | 61 | func testSetFailureTypePublisher() { 62 | let expectation = XCTestExpectation(description: debugDescription) 63 | 64 | let initialSequence = ["one", "two", "red", "blue"] 65 | 66 | let cancellable = initialSequence.publisher 67 | .setFailureType(to: TestFailureCondition.self) 68 | .sink(receiveCompletion: { completion in 69 | print(".sink() received the completion", String(describing: completion)) 70 | switch completion { 71 | case .finished: 72 | break 73 | case let .failure(anError): 74 | print("received error: ", anError) 75 | } 76 | expectation.fulfill() 77 | }, receiveValue: { responseValue in 78 | print(".sink() data received \(responseValue)") 79 | }) 80 | 81 | wait(for: [expectation], timeout: 1.0) 82 | XCTAssertNotNil(cancellable) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /UsingCombineTests/FilterPublisherTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FilterPublisherTests.swift 3 | // UsingCombineTests 4 | // 5 | // Created by Joseph Heck on 7/11/19. 6 | // Copyright © 2019 SwiftUI-Notes. All rights reserved. 7 | // 8 | 9 | import Combine 10 | import XCTest 11 | 12 | class FilterPublisherTests: XCTestCase { 13 | func testFilter() { 14 | let simplePublisher = PassthroughSubject() 15 | 16 | let cancellable = simplePublisher 17 | .filter { stringValue in 18 | stringValue == "onefish" 19 | } 20 | .print(debugDescription) 21 | .sink(receiveCompletion: { completion in 22 | print(".sink() received the completion:", String(describing: completion)) 23 | switch completion { 24 | case let .failure(anError): 25 | print(".sink() received completion error: ", anError) 26 | XCTFail("no error should be received") 27 | case .finished: 28 | break 29 | } 30 | }, receiveValue: { stringValue in 31 | print(".sink() received \(stringValue)") 32 | XCTAssertEqual(stringValue, "onefish") 33 | }) 34 | 35 | simplePublisher.send("onefish") // onefish will pass the filter 36 | simplePublisher.send("twofish") // twofish will not 37 | simplePublisher.send(completion: Subscribers.Completion.finished) 38 | XCTAssertNotNil(cancellable) 39 | } 40 | 41 | func testTryFilter() { 42 | enum TestFailure: Error { 43 | case boom 44 | } 45 | 46 | let simplePublisher = PassthroughSubject() 47 | 48 | let cancellable = simplePublisher 49 | .tryFilter { stringValue in 50 | if stringValue == "explode" { 51 | throw TestFailure.boom 52 | } 53 | return stringValue == "onefish" 54 | } 55 | .print(debugDescription) 56 | .sink(receiveCompletion: { completion in 57 | print(".sink() received the completion:", String(describing: completion)) 58 | switch completion { 59 | case let .failure(anError): 60 | print(".sink() received completion error: ", anError) 61 | case .finished: 62 | XCTFail("test sequence should fail before receiving finished") 63 | } 64 | }, receiveValue: { stringValue in 65 | print(".sink() received \(stringValue)") 66 | XCTAssertEqual(stringValue, "onefish") 67 | }) 68 | 69 | simplePublisher.send("onefish") // onefish will pass the filter 70 | simplePublisher.send("twofish") // twofish will not 71 | simplePublisher.send("explode") // explode will trigger a failure 72 | simplePublisher.send(completion: Subscribers.Completion.finished) 73 | XCTAssertNotNil(cancellable) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /UsingCombineTests/HandleEventsPublisherTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HandleEventsPublisherTests.swift 3 | // UsingCombineTests 4 | // 5 | // Created by Joseph Heck on 7/11/19. 6 | // Copyright © 2019 SwiftUI-Notes. All rights reserved. 7 | // 8 | 9 | import Combine 10 | import XCTest 11 | 12 | class HandleEventsPublisherTests: XCTestCase { 13 | func testHandleEvents() { 14 | let publisher = PassthroughSubject() 15 | 16 | // this sets up the chain of whatever it's going to do 17 | let cancellable = publisher 18 | .handleEvents(receiveSubscription: { aValue in 19 | print("receiveSubscription event called with \(String(describing: aValue))") 20 | // this happened second: 21 | // receiveSubscription event called with PassthroughSubject 22 | XCTAssertNotNil(aValue) // type returned is a Subscription 23 | }, receiveOutput: { aValue in 24 | // third: 25 | // handle events gives us an interesting window into all the flow mechanisms that 26 | // can happen during the Publish/Subscribe conversation, including capturing when 27 | // we receive completions, values, etc 28 | print("receiveOutput was invoked with \(String(describing: aValue))") 29 | XCTAssertEqual(aValue, "DATA IN") 30 | }, receiveCompletion: { aValue in 31 | // completion .finished were sent in this test 32 | print("receiveCompletion event called with \(String(describing: aValue))") 33 | }, receiveCancel: { 34 | // no cancellations sent in this test 35 | print("receiveCancel event invoked") 36 | XCTFail("cancel should not be received in this test") 37 | }, receiveRequest: { aValue in 38 | print("receiveRequest event called with \(String(describing: aValue))") 39 | // this happened first: 40 | // receiveRequest event called with unlimited 41 | XCTAssertEqual(aValue, Subscribers.Demand.unlimited) 42 | }) 43 | .sink(receiveValue: { aValue in 44 | // sink captures and terminates the pipeline of operators 45 | print("sink captured the result of \(String(describing: aValue))") 46 | }) 47 | 48 | publisher.send("DATA IN") 49 | publisher.send(completion: .finished) 50 | XCTAssertNotNil(cancellable) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /UsingCombineTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /UsingCombineTests/InterimTestingStructs.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InterimTestingStructs.swift 3 | // UsingCombineTests 4 | // 5 | // Created by Joseph Heck on 7/20/19. 6 | // Copyright © 2019 SwiftUI-Notes. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /* 12 | Generic interim tuples, specifically for testing equality from functions 13 | where tuples are return or used heavily instead of explicit structs. 14 | */ 15 | struct Tuple2 { 16 | let t0: T0 17 | let t1: T1 18 | 19 | init(_ tuple: (T0, T1)) { 20 | t0 = tuple.0 21 | t1 = tuple.1 22 | } 23 | 24 | var raw: (T0, T1) { (t0, t1) } 25 | } 26 | 27 | extension Tuple2: Equatable where T0: Equatable, T1: Equatable {} 28 | extension Tuple2: Hashable where T0: Hashable, T1: Hashable {} 29 | 30 | struct Tuple3 { 31 | let t0: T0 32 | let t1: T1 33 | let t2: T2 34 | 35 | init(_ tuple: (T0, T1, T2)) { 36 | t0 = tuple.0 37 | t1 = tuple.1 38 | t2 = tuple.2 39 | } 40 | 41 | var raw: (T0, T1, T2) { (t0, t1, t2) } 42 | } 43 | 44 | extension Tuple3: Equatable where T0: Equatable, T1: Equatable, T2: Equatable {} 45 | extension Tuple3: Hashable where T0: Hashable, T1: Hashable, T2: Hashable {} 46 | 47 | struct Tuple4 { 48 | let t0: T0 49 | let t1: T1 50 | let t2: T2 51 | let t3: T3 52 | 53 | init(_ tuple: (T0, T1, T2, T3)) { 54 | t0 = tuple.0 55 | t1 = tuple.1 56 | t2 = tuple.2 57 | t3 = tuple.3 58 | } 59 | 60 | var raw: (T0, T1, T2, T3) { (t0, t1, t2, t3) } 61 | } 62 | 63 | extension Tuple4: Equatable where T0: Equatable, T1: Equatable, T2: Equatable, T3: Equatable {} 64 | extension Tuple4: Hashable where T0: Hashable, T1: Hashable, T2: Hashable, T3: Hashable {} 65 | -------------------------------------------------------------------------------- /UsingCombineTests/MeasureIntervalTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MeasureIntervalTests.swift 3 | // UsingCombineTests 4 | // 5 | // Created by Joseph Heck on 12/15/19. 6 | // Copyright © 2019 SwiftUI-Notes. All rights reserved. 7 | // 8 | 9 | import Combine 10 | import XCTest 11 | 12 | class MeasureIntervalTests: XCTestCase { 13 | func testMeasureInterval() { 14 | let foo = PassthroughSubject() 15 | // no initial value is propagated from a PassthroughSubject 16 | 17 | let q = DispatchQueue(label: debugDescription) 18 | let expectation = XCTestExpectation(description: debugDescription) 19 | 20 | var receivedList: [DispatchQueue.SchedulerTimeType.Stride] = [] 21 | 22 | let cancellable = foo 23 | .measureInterval(using: q) // DispatchQueue.SchedulerTimeType.Stride 24 | .print(debugDescription) 25 | .sink { someValue in 26 | print("Magniture updated to: ", someValue.magnitude, " interval: ", someValue.timeInterval) 27 | receivedList.append(someValue) 28 | } 29 | 30 | q.asyncAfter(deadline: .now() + 0.1) { 31 | print("sending value on queue", String(cString: __dispatch_queue_get_label(nil), encoding: .utf8)!) 32 | foo.send(1) 33 | // Stride received. Magniture updated to: 110454274 interval: nanoseconds(110454274) 34 | } 35 | q.asyncAfter(deadline: .now() + 0.2) { 36 | print("sending value on queue", String(cString: __dispatch_queue_get_label(nil), encoding: .utf8)!) 37 | foo.send(2) 38 | // Stride received. Magniture updated to: 107415192 interval: nanoseconds(107415192) 39 | } 40 | q.asyncAfter(deadline: .now() + 1.1) { 41 | print("sending value on queue", String(cString: __dispatch_queue_get_label(nil), encoding: .utf8)!) 42 | foo.send(3) 43 | // Stride received. Magniture updated to: 887884605 interval: nanoseconds(887884605) 44 | } 45 | q.asyncAfter(deadline: .now() + 1.2) { 46 | print("sending value on queue", String(cString: __dispatch_queue_get_label(nil), encoding: .utf8)!) 47 | foo.send(4) 48 | // Stride received. Magniture updated to: 120933362 interval: nanoseconds(120933362) 49 | } 50 | q.asyncAfter(deadline: .now() + 1.21) { 51 | print("sending value on queue", String(cString: __dispatch_queue_get_label(nil), encoding: .utf8)!) 52 | foo.send(5) 53 | // Stride received. Magniture updated to: 115129 interval: nanoseconds(115129) 54 | } 55 | 56 | q.asyncAfter(deadline: .now() + 3) { 57 | expectation.fulfill() 58 | } 59 | 60 | wait(for: [expectation], timeout: 5.0) 61 | XCTAssertEqual(receivedList.count, 5) 62 | XCTAssertNotNil(cancellable) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /UsingCombineTests/MergeManyPublisherTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MergeManyPublisherTests.swift 3 | // UsingCombineTests 4 | // 5 | // Created by Евгений Орехин on 21.03.2021. 6 | // Copyright © 2021 SwiftUI-Notes. All rights reserved. 7 | // 8 | 9 | import Combine 10 | import XCTest 11 | 12 | final class MergeManyPublisherTests: XCTestCase { 13 | private let maxDelay: TimeInterval = 10.0 14 | 15 | /// Return the asyncronus publisher that send delay time as output after this delay 16 | private func createDelayedAsyncPublisher(minDelay: TimeInterval = 1.0) -> AnyPublisher { 17 | let pub = Deferred { 18 | Future { promise in 19 | let delay = TimeInterval(Int.random(in: Int(minDelay) ... Int(self.maxDelay))) 20 | DispatchQueue.main.asyncAfter(deadline: .now() + delay) { 21 | promise(.success(delay)) 22 | } 23 | } 24 | } 25 | return pub.eraseToAnyPublisher() 26 | } 27 | 28 | func testMergeManyAsyncPublishersWithSyncronizedTerminating() { 29 | let expectation = XCTestExpectation(description: debugDescription) 30 | 31 | var output: [TimeInterval] = [] 32 | 33 | var delayedAsyncPublishers = (0 ..< 5).map { _ in 34 | self.createDelayedAsyncPublisher() 35 | } 36 | 37 | delayedAsyncPublishers.append(createDelayedAsyncPublisher(minDelay: maxDelay)) 38 | 39 | let mergedPublishers = Publishers.MergeMany(delayedAsyncPublishers) 40 | 41 | let cancellable = mergedPublishers 42 | .collect() 43 | .sink(receiveValue: { value in 44 | output = value 45 | expectation.fulfill() 46 | }) 47 | 48 | wait(for: [expectation], timeout: maxDelay) 49 | 50 | XCTAssertEqual(output.count, 51 | 6, 52 | "The output count must be equal to 6 because of using collect operator and waiting output as long as max time interval delay") 53 | 54 | XCTAssertTrue(output.max() ?? 0 == maxDelay, 55 | "The max value of output must be equal to the maximum delay of async operation \(maxDelay)") 56 | 57 | XCTAssertNotNil(cancellable) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /UsingCombineTests/Mocker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Mocker.swift 3 | // Rabbit 4 | // 5 | // Created by Antoine van der Lee on 04/05/2017. 6 | // Copyright © 2017 WeTransfer. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Can be used for registering Mocked data, returned by the `MockingURLProtocol`. 12 | public struct Mocker { 13 | public enum HTTPVersion: String { 14 | case http1_0 = "HTTP/1.0" 15 | case http1_1 = "HTTP/1.1" 16 | case http2_0 = "HTTP/2.0" 17 | } 18 | 19 | /// The shared instance of the Mocker, can be used to register and return mocks. 20 | internal static var shared = Mocker() 21 | 22 | /// The HTTP Version to use in the mocked response. 23 | public static var httpVersion: HTTPVersion = .http1_1 24 | 25 | /// The registrated mocks. 26 | private(set) var mocks: [Mock] = [] 27 | 28 | /// URLs to ignore for mocking. 29 | private(set) var ignoredURLs: [URL] = [] 30 | 31 | private init() { 32 | // Whenever someone is requesting the Mocker, we want the URL protocol to be activated. 33 | URLProtocol.registerClass(MockingURLProtocol.self) 34 | } 35 | 36 | /// Register new Mocked data. If a mock for the same URL and HTTPMethod exists, it will be overwritten. 37 | /// 38 | /// - Parameter mock: The Mock to be registered for future requests. 39 | public static func register(_ mock: Mock) { 40 | /// Delete the Mock if it was already registered. 41 | shared.mocks.removeAll(where: { $0 == mock }) 42 | shared.mocks.append(mock) 43 | } 44 | 45 | /// Register an URL to ignore for mocking. This will let the URL work as if the Mocker doesn't exist. 46 | /// 47 | /// - Parameter url: The URL to mock. 48 | public static func ignore(_ url: URL) { 49 | shared.ignoredURLs.append(url) 50 | } 51 | 52 | /// Checks if the passed URL should be handled by the Mocker. If the URL is registered to be ignored, it will not handle the URL. 53 | /// 54 | /// - Parameter url: The URL to check for. 55 | /// - Returns: `true` if it should be mocked, `false` if the URL is registered as ignored. 56 | public static func shouldHandle(_ url: URL) -> Bool { 57 | return !shared.ignoredURLs.contains(url) 58 | } 59 | 60 | /// Removes all registered mocks. Use this method in your tearDown function to make sure a Mock is not used in any other test. 61 | public static func removeAll() { 62 | shared.mocks.removeAll() 63 | } 64 | 65 | /// Retrieve a Mock for the given request. Matches on `request.url` and `request.httpMethod`. 66 | /// 67 | /// - Parameter request: The request to search for a mock. 68 | /// - Returns: A mock if found, `nil` if there's no mocked data registered for the given request. 69 | static func mock(for request: URLRequest) -> Mock? { 70 | /// First check for specific URLs 71 | if let specificMock = shared.mocks.first(where: { $0 == request && $0.fileExtensions == nil }) { 72 | return specificMock 73 | } 74 | /// Second, check for generic file extension Mocks 75 | return shared.mocks.first(where: { $0 == request }) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /UsingCombineTests/MockingURLProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockingURLProtocol.swift 3 | // Rabbit 4 | // 5 | // Created by Antoine van der Lee on 04/05/2017. 6 | // Copyright © 2017 WeTransfer. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// The protocol which can be used to send Mocked data back. Use the `Mocker` to register `Mock` data 12 | public final class MockingURLProtocol: URLProtocol { 13 | enum Error: Swift.Error { 14 | case missingMockedData(url: String) 15 | case explicitMockFailure(url: String) 16 | } 17 | 18 | /// Returns Mocked data based on the mocks register in the `Mocker`. Will end up in an error when no Mock data is found for the request. 19 | override public func startLoading() { 20 | guard 21 | let mock = Mocker.mock(for: request), 22 | let response = HTTPURLResponse(url: mock.url, statusCode: mock.statusCode, httpVersion: Mocker.httpVersion.rawValue, headerFields: mock.headers), 23 | let data = mock.data(for: request) 24 | else { 25 | // swiftlint:disable nslog_prohibited 26 | print("\n\n 🚨 No mocked data found for url \(String(describing: request.url?.absoluteString)) method \(String(describing: request.httpMethod)). Did you forget to use `register()`? 🚨 \n\n") 27 | client?.urlProtocol(self, didFailWithError: Error.missingMockedData(url: String(describing: request.url?.absoluteString))) 28 | return 29 | } 30 | 31 | DispatchQueue.global(qos: DispatchQoS.QoSClass.background).asyncAfter(deadline: .now() + (mock.delay ?? DispatchTimeInterval.seconds(0))) { 32 | if let redirectLocation = data.redirectLocation { 33 | self.client?.urlProtocol(self, wasRedirectedTo: URLRequest(url: redirectLocation), redirectResponse: response) 34 | } else { 35 | if mock.reportFailure { 36 | self.client?.urlProtocol(self, didFailWithError: Error.explicitMockFailure(url: String(describing: self.request.url?.absoluteString))) 37 | } else { 38 | self.client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) 39 | self.client?.urlProtocol(self, didLoad: data) 40 | self.client?.urlProtocolDidFinishLoading(self) 41 | } 42 | } 43 | mock.completion?() 44 | } 45 | } 46 | 47 | /// Overrides needed to define a valid inheritance of URLProtocol. 48 | override public class func canInit(with request: URLRequest) -> Bool { 49 | guard let url = request.url else { return false } 50 | return Mocker.shouldHandle(url) 51 | } 52 | 53 | /// Implementation does nothing, but is needed for a valid inheritance of URLProtocol. 54 | override public func stopLoading() { 55 | // No implementation needed 56 | } 57 | 58 | /// Simply sends back the passed request. Implementation is needed for a valid inheritance of URLProtocol. 59 | override public class func canonicalRequest(for request: URLRequest) -> URLRequest { 60 | return request 61 | } 62 | } 63 | 64 | private extension Data { 65 | /// Returns the redirect location from the raw HTTP response if exists. 66 | var redirectLocation: URL? { 67 | let locationComponent = String(data: self, encoding: String.Encoding.utf8)?.components(separatedBy: "\n").first(where: { value -> Bool in 68 | value.contains("Location:") 69 | }) 70 | 71 | guard let redirectLocationString = locationComponent?.components(separatedBy: "Location:").last, let redirectLocation = URL(string: redirectLocationString.trimmingCharacters(in: NSCharacterSet.whitespaces)) else { 72 | return nil 73 | } 74 | return redirectLocation 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /UsingCombineTests/ObservableObjectPublisherTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ObservableObjectPublisherTests.swift 3 | // UsingCombineTests 4 | // 5 | // Created by Joseph Heck on 8/11/19. 6 | // Copyright © 2019 SwiftUI-Notes. All rights reserved. 7 | // 8 | 9 | import Combine 10 | import XCTest 11 | 12 | class ObservableObjectPublisherTests: XCTestCase { 13 | func testCodeExample() { 14 | let expectation = XCTestExpectation(description: debugDescription) 15 | class Contact: ObservableObject { 16 | @Published var name: String 17 | @Published var age: Int 18 | 19 | init(name: String, age: Int) { 20 | self.name = name 21 | self.age = age 22 | } 23 | 24 | func haveBirthday() -> Int { 25 | age += 1 26 | return age 27 | } 28 | } 29 | 30 | let john = Contact(name: "John Appleseed", age: 24) 31 | let cancellable = john.objectWillChange.sink { _ in 32 | expectation.fulfill() 33 | print("will change") 34 | } 35 | print(john.haveBirthday()) 36 | // Prints "will change" 37 | // Prints "25" 38 | 39 | wait(for: [expectation], timeout: 5.0) 40 | XCTAssertNotNil(cancellable) 41 | } 42 | 43 | class ExampleObject: ObservableObject { 44 | @Published var someProperty: String 45 | 46 | init(someProperty: String) { 47 | self.someProperty = someProperty 48 | } 49 | 50 | func shoutProperty() -> String { 51 | // this function is an example of something changing a published property 52 | someProperty = someProperty.uppercased() 53 | return someProperty 54 | } 55 | } 56 | 57 | func testObservableObjectPublisher() { 58 | let expectation = XCTestExpectation(description: debugDescription) 59 | 60 | let example = ExampleObject(someProperty: "quietly, please") 61 | 62 | XCTAssertNotNil(example.objectWillChange) 63 | let cancellable = example.objectWillChange 64 | .print("cancellable") 65 | .sink(receiveCompletion: { completion in 66 | print(".sink() received the completion", String(describing: completion)) 67 | switch completion { 68 | case .finished: 69 | XCTFail("No finished should be received from empty") 70 | case let .failure(anError): 71 | XCTFail("No failure should be received from empty") 72 | print("received error: ", anError) 73 | } 74 | }, receiveValue: { valueReceived in 75 | XCTAssertNotNil(valueReceived) 76 | // `valueReceived` is of type ObservableObject.Output, which is type-aliased in Foundation to Void... 77 | // so while it's not "nil", it'll never have any sort of real value - it's just a token 78 | // to trigger the pipeline and should generally be taken in as an ignored value. 79 | expectation.fulfill() 80 | print(".sink() data received \(valueReceived)") 81 | }) 82 | 83 | XCTAssertEqual(example.someProperty, "quietly, please") 84 | let result = example.shoutProperty() 85 | XCTAssertEqual(result, "QUIETLY, PLEASE") 86 | 87 | wait(for: [expectation], timeout: 5.0) 88 | XCTAssertNotNil(cancellable) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /UsingCombineTests/TimerPublisherTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TimerPublisherTests.swift 3 | // UsingCombineTests 4 | // 5 | // Created by Joseph Heck on 7/30/19. 6 | // Copyright © 2019 SwiftUI-Notes. All rights reserved. 7 | // 8 | 9 | import Combine 10 | import XCTest 11 | 12 | class TimerPublisherTests: XCTestCase { 13 | func testTimerPublisherWithAutoconnect() { 14 | let expectation = XCTestExpectation(description: debugDescription) 15 | let q = DispatchQueue(label: debugDescription) 16 | var countOfReceivedEvents = 0 17 | 18 | let cancellable = Timer.publish(every: 1.0, on: RunLoop.main, in: .common) 19 | .autoconnect() 20 | .sink { receivedTimeStamp in 21 | // type is Date 22 | print("passed through: ", receivedTimeStamp) 23 | XCTAssertNotNil(receivedTimeStamp) 24 | countOfReceivedEvents += 1 25 | } 26 | 27 | q.asyncAfter(deadline: .now() + 3.4) { 28 | expectation.fulfill() 29 | } 30 | 31 | XCTAssertNotNil(cancellable) 32 | wait(for: [expectation], timeout: 5.0) 33 | XCTAssertEqual(countOfReceivedEvents, 3) 34 | } 35 | 36 | func testTimerPublisherWithConnect() { 37 | let expectation = XCTestExpectation(description: debugDescription) 38 | let q = DispatchQueue(label: debugDescription) 39 | var countOfReceivedEvents = 0 40 | 41 | let timerPublisher = Timer.publish(every: 1.0, on: RunLoop.main, in: .common) 42 | let cancellable = timerPublisher 43 | .sink { receivedTimeStamp in 44 | print("passed through: ", receivedTimeStamp) 45 | XCTAssertNotNil(receivedTimeStamp) 46 | countOfReceivedEvents += 1 47 | } 48 | 49 | q.asyncAfter(deadline: .now() + 1.0) { 50 | let connectCancellable = timerPublisher.connect() 51 | XCTAssertNotNil(connectCancellable) 52 | } 53 | 54 | q.asyncAfter(deadline: .now() + 3.4) { 55 | expectation.fulfill() 56 | } 57 | 58 | XCTAssertNotNil(cancellable) 59 | wait(for: [expectation], timeout: 5.0) 60 | XCTAssertEqual(countOfReceivedEvents, 2) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /docs/images/UsingCombineWithSwiftCoverArt_16x9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heckj/swiftui-notes/e7884cce9b472b143a26a0b755e0f5ebc5b4f9dc/docs/images/UsingCombineWithSwiftCoverArt_16x9.png -------------------------------------------------------------------------------- /docs/images/UsingCombineWithSwiftCoverArt_2560x1920.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heckj/swiftui-notes/e7884cce9b472b143a26a0b755e0f5ebc5b4f9dc/docs/images/UsingCombineWithSwiftCoverArt_2560x1920.png -------------------------------------------------------------------------------- /docs/images/cloneRepository.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heckj/swiftui-notes/e7884cce9b472b143a26a0b755e0f5ebc5b4f9dc/docs/images/cloneRepository.png -------------------------------------------------------------------------------- /docs/images/diagrams/basic_types.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Produced by OmniGraffle 7.13 12 | 2020-02-27 19:31:19 +0000 13 | 14 | 15 | basic_types 16 | 17 | Layer 1 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | Failure Type 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | Output Type 47 | 48 | 49 | 50 | 51 | 52 | written as <Output, Failure> 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /docs/images/diagrams/endings.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 28 | 33 | 39 | 44 | 49 | 55 | 56 | 64 | 68 | 69 | 70 | 71 | 72 | 76 | 77 | 81 | 82 | 83 | 85 | 86 | 87 | 91 | 92 | 93 | 97 | 101 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /docs/images/diagrams/marble_diagram.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | Produced by OmniGraffle 7.13 17 | 2020-02-27 20:00:01 +0000 18 | 19 | 20 | marble_diagram 21 | 22 | Layer 1 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | operator 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /docs/images/mars.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heckj/swiftui-notes/e7884cce9b472b143a26a0b755e0f5ebc5b4f9dc/docs/images/mars.png -------------------------------------------------------------------------------- /docs/images/welcomeToXcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heckj/swiftui-notes/e7884cce9b472b143a26a0b755e0f5ebc5b4f9dc/docs/images/welcomeToXcode.png -------------------------------------------------------------------------------- /docs/lib/git-metadata-preprocessor.rb: -------------------------------------------------------------------------------- 1 | RUBY_ENGINE == 'opal' ? 2 | (require 'git-metadata-preprocessor/extension') : 3 | (require_relative 'git-metadata-preprocessor/extension') 4 | 5 | Asciidoctor::Extensions.register do 6 | preprocessor GitMetadataPreprocessor 7 | end 8 | -------------------------------------------------------------------------------- /docs/lib/git-metadata-preprocessor/extension.rb: -------------------------------------------------------------------------------- 1 | require 'asciidoctor/extensions' unless RUBY_ENGINE == 'opal' 2 | 3 | require 'rugged' 4 | require 'pathname' 5 | 6 | class GitMetadataPreprocessor < Asciidoctor::Extensions::Preprocessor 7 | def process document, reader 8 | 9 | begin 10 | repo = Rugged::Repository.discover('.') 11 | rescue 12 | $stderr.puts('Failed to find repository, git-metadata extension terminating') 13 | return 14 | end 15 | 16 | if repo.empty? || repo.bare? 17 | $stderr.puts('Repository is empty or bare repository, git-metadata extension terminating') 18 | return 19 | end 20 | 21 | doc_attrs = document.attributes 22 | head = repo.head 23 | 24 | doc_attrs['git-metadata-sha'] = head.target_id 25 | doc_attrs['git-metadata-sha-short'] = head.target_id.slice 0, 7 26 | doc_attrs['git-metadata-author-name'] = head.target.author[:name] 27 | doc_attrs['git-metadata-author-email'] = head.target.author[:email] 28 | doc_attrs['git-metadata-date'] = head.target.time.strftime '%Y-%m-%d' 29 | doc_attrs['git-metadata-time'] = head.target.time.strftime '%H:%M:%S' 30 | doc_attrs['git-metadata-timezone'] = head.target.time.strftime '%Z' 31 | doc_attrs['git-metadata-commit-message'] = head.target.message 32 | 33 | if repo.head_detached? 34 | doc_attrs['git-metadata-branch'] = 'HEAD detached' 35 | elsif repo.head_unborn? 36 | doc_attrs['git-metadata-branch'] = 'HEAD unborn' 37 | else 38 | doc_attrs['git-metadata-branch'] = repo.branches[head.name].name 39 | end 40 | 41 | tags = repo.tags 42 | .select {|t| t.target_id == head.target_id || (t.annotated? && t.annotation.target_id == head.target_id) } 43 | .map(&:name) 44 | doc_attrs['git-metadata-tag'] = tags * ', ' unless tags.empty? 45 | 46 | file_location = Pathname.new Dir.pwd 47 | repo_location = Pathname.new File.dirname(repo.path) # repo.path uses the .git directory 48 | doc_attrs['git-metadata-relative-path'] = repo_location.relative_path_from file_location 49 | doc_attrs['git-metadata-repo-path'] = repo_location.realpath 50 | 51 | if repo.remotes['origin'] 52 | doc_attrs['git-metadata-remotes-origin'] = repo.remotes['origin'].url 53 | end 54 | 55 | nil 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /docs/lib/git-metadata-preprocessor/sample.adoc: -------------------------------------------------------------------------------- 1 | = Test Document 2 | 3 | [cols="1,m,m,1",options="header"] 4 | |=== 5 | |Item 6 | |Example from this repository 7 | |Attribute Name 8 | |Comment 9 | 10 | |latest commit sha 11 | |{git-metadata-sha} 12 | |git-metadata-sha 13 | | 14 | 15 | |short (7-char) commit sha 16 | |{git-metadata-sha-short} 17 | |git-metadata-sha-short 18 | |NB: Git tools and Github use 7 chars, Gitlab folks use 8 chars. 19 | 20 | |latest commit author 21 | |{git-metadata-author-name} 22 | |git-metadata-author-name 23 | | 24 | 25 | |latest commit author email 26 | |{git-metadata-author-email} 27 | |git-metadata-author-email 28 | | 29 | 30 | |latest commit date 31 | |{git-metadata-date} 32 | |git-metadata-date 33 | | 34 | 35 | |latest commit time 36 | |{git-metadata-time} 37 | |git-metadata-time 38 | | 39 | 40 | |latest commit timezone 41 | |{git-metadata-timezone} 42 | |git-metadata-timezone 43 | | 44 | 45 | |latest commit message 46 | |{git-metadata-commit-message} 47 | |git-metadata-commit-message 48 | | 49 | 50 | |latest commit message 51 | |{git-metadata-commit-message} 52 | |git-metadata-commit-message 53 | | 54 | 55 | |the current branch 56 | |{git-metadata-branch} 57 | |git-metadata-branch 58 | |if head is detached, returns `HEAD detached` 59 | 60 | |the current tags(s) 61 | |{git-metadata-tag} 62 | |git-metadata-tag 63 | |only defined if there are tags. 64 | If there are multiple tags they are combined with a ``, ``. 65 | Does both annotated and lightweight tags. 66 | 67 | |the path to this repository 68 | |{git-metadata-repo-path} 69 | |git-metadata-repo-path 70 | | 71 | 72 | |the relative path from this file to the repository root 73 | |{git-metadata-relative-path} 74 | |git-metadata-relative-path 75 | |useful for relative referencing and locating static resources 76 | 77 | |the `origin` remote if exists 78 | |{git-metadata-remotes-origin} 79 | |git-metadata-remotes-origin 80 | |empty if the remote does not exist 81 | |=== 82 | 83 | Improvements and additions would be most welcome! 84 | 85 | == How can you use this? 86 | 87 | === An automatically updating version 88 | 89 | I like to do something like the following in my document header for versioning purposes: 90 | 91 | ---- 92 | \ifeval::["{git-metadata-branch}" == "master"] 93 | :version: {git-metadata-branch}-{git-metadata-sha-short} 94 | \endif::[] 95 | \ifeval::["{git-metadata-branch}" != "master"] 96 | :version: {git-metadata-branch} 97 | \endif::[] 98 | ---- 99 | 100 | or perhaps: 101 | 102 | ---- 103 | \ifdef::git-metadata-tag[] 104 | :version: {git-metadata-tag} 105 | \endif::[] 106 | \ifndef::git-metadata-tag[] 107 | :version: {git-metadata-branch}-{git-metadata-sha-short} 108 | \endif::[] 109 | ---- 110 | 111 | === Locating assets used by multiple documents 112 | 113 | I use the following when I have a range of asciidoc files in different places in a repository. 114 | I want them to all have perhaps a common stylesheet or other information. 115 | In this case I want to always pull in `attributes.adoc`. 116 | The `git-metadata-relative-path` gives the path from the current document back to the repository root. 117 | This saves the user needing to specify it for each document. 118 | 119 | ---- 120 | :assetdir: {git-metadata-relative-path}/assets 121 | :stylesdir: {assetdir} 122 | \include::{assetdir}/attributes.adoc[] 123 | ---- 124 | -------------------------------------------------------------------------------- /docs/lib/google-analytics-docinfoprocessor.rb: -------------------------------------------------------------------------------- 1 | require 'asciidoctor/extensions' unless RUBY_ENGINE == 'opal' 2 | 3 | # A docinfo processor that appends the Google Analytics code to the bottom of the HTML. 4 | # 5 | # Usage 6 | # 7 | # :google-analytics-account: UA-XXXXXXXX-1 8 | # 9 | # Requires Asciidoctor >= 1.5.2 10 | Asciidoctor::Extensions.register do 11 | if @document.basebackend? 'html' 12 | docinfo_processor do 13 | at_location :footer 14 | process do |doc| 15 | next unless (ga_account_id = doc.attr 'google-analytics-account') 16 | %() 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /docs/pattern-assertnofailure.adoc: -------------------------------------------------------------------------------- 1 | [#patterns-assertnofailure] 2 | == Verifying a failure hasn't happened using assertNoFailure 3 | 4 | __Goal__:: 5 | 6 | * Verify no error has occurred within a pipeline 7 | 8 | __References__:: 9 | 10 | * <> 11 | 12 | __See also__:: 13 | 14 | * <> 15 | * <> 16 | 17 | __Code and explanation__:: 18 | 19 | Useful in testing invariants in pipelines, the assertNoFailure operator also converts the failure type to ``. 20 | The operator will cause the application to terminate (or tests to crash to a debugger) if the assertion is triggered. 21 | 22 | This is useful for verifying the invariant of having dealt with an error. 23 | If you are sure you handled the errors and need to map a pipeline which technically can generate a failure type of `` to a subscriber that requires a failure type of ``. 24 | 25 | It is far more likely that you want to handle the error with and not have the application terminate. 26 | Look forward to <> and <> for patterns of how to provide logic to handle errors in a pipeline. 27 | 28 | // force a page break - in HTML rendering is just a
29 | <<< 30 | ''' 31 | -------------------------------------------------------------------------------- /docs/pattern-assign.adoc: -------------------------------------------------------------------------------- 1 | [#patterns-assign-subscriber] 2 | == Creating a subscriber with assign 3 | 4 | __Goal__:: 5 | 6 | * To use the results of a pipeline to set a value, often a property on a user interface view or control, but any KVO compliant object can be the provider. 7 | 8 | __References__:: 9 | 10 | * <> 11 | * <> 12 | 13 | __See also__:: 14 | 15 | * <> 16 | 17 | __Code and explanation__:: 18 | 19 | Assign is a subscriber that's specifically designed to apply data from a publisher or pipeline into a property, updating that property whenever it receives data. 20 | Like sink, it activates when created and requests an unlimited data. 21 | Assign requires the failure type to be specified as ``, so if your pipeline could fail (such as using an operator like tryMap) you will need to <> before using `.assign`. 22 | 23 | .simple assign 24 | [source, swift] 25 | ---- 26 | let cancellablePipeline = publishingSource <1> 27 | .receive(on: RunLoop.main) <2> 28 | .assign(to: \.isEnabled, on: yourButton) <3> 29 | 30 | cancellablePipeline.cancel() <4> 31 | ---- 32 | 33 | <1> `.assign` is typically chained onto a publisher when you create it, and the return value is cancellable. 34 | <2> If `.assign` is being used to update a user interface element, you need to make sure that it is being updated on the main thread. This call makes sure the subscriber is received on the main thread. 35 | <3> Assign references the property being updated using a https://developer.apple.com/documentation/swift/referencewritablekeypath[key path], and a reference to the object being updated. 36 | <4> At any time you can cancel to terminate and invalidate pipelines with `cancel()`. Frequently, you cancel the pipelines when you deactivate the objects (such as a viewController) that are getting updated from the pipeline. 37 | 38 | // force a page break - in HTML rendering is just a
39 | <<< 40 | ''' 41 | -------------------------------------------------------------------------------- /docs/pattern-constrained-network.adoc: -------------------------------------------------------------------------------- 1 | [#patterns-constrained-network] 2 | == Requesting data from an alternate URL when the network is constrained 3 | 4 | __Goal__:: 5 | 6 | * From Apple's WWDC 2019 presentation https://developer.apple.com/videos/play/wwdc2019/712/[Advances in Networking, Part 1], a sample pattern was provided using `tryCatch` and `tryMap` operators to react to the specific error of the network being constrained. 7 | 8 | __References__:: 9 | 10 | * <> 11 | * <> 12 | * <> 13 | 14 | __See also__:: 15 | 16 | * <> 17 | * <> 18 | 19 | __Code and explanation__:: 20 | 21 | [source, swift] 22 | ---- 23 | // Generalized Publisher for Adaptive URL Loading 24 | func adaptiveLoader(regularURL: URL, lowDataURL: URL) -> AnyPublisher { 25 | var request = URLRequest(url: regularURL) <1> 26 | request.allowsConstrainedNetworkAccess = false <2> 27 | return URLSession.shared.dataTaskPublisher(for: request) <3> 28 | .tryCatch { error -> URLSession.DataTaskPublisher in <4> 29 | guard error.networkUnavailableReason == .constrained else { 30 | throw error 31 | } 32 | return URLSession.shared.dataTaskPublisher(for: lowDataURL) <5> 33 | .tryMap { data, response -> Data in 34 | guard let httpResponse = response as? HTTPUrlResponse, <6> 35 | httpResponse.statusCode == 200 else { 36 | throw MyNetworkingError.invalidServerResponse 37 | } 38 | return data 39 | } 40 | .eraseToAnyPublisher() <7> 41 | ---- 42 | 43 | This example, from Apple's WWDC, provides a function that takes two URLs - a primary and a fallback. 44 | It returns a publisher that will request data and fall back requesting a secondary URL when the network is constrained. 45 | 46 | <1> The request starts with an attempt requesting data. 47 | <2> Setting `request.allowsConstrainedNetworkAccess` will cause the `dataTaskPublisher` to error if the network is constrained. 48 | <3> Invoke the `dataTaskPublisher` to make the request. 49 | <4> `tryCatch` is used to capture the immediate error condition and check for a specific error (the constrained network). 50 | <5> If it finds an error, it creates a new one-shot publisher with the fall-back URL. 51 | <6> The resulting publisher can still fail, and `tryMap` can map this a failure by throwing an error on HTTP response codes that map to error conditions 52 | <7> `eraseToAnyPublisher` enables type erasure on the chain of operators so the resulting signature of the adaptiveLoader function is `AnyPublisher` 53 | 54 | In the sample, if the error returned from the original request wasn't an issue of the network being constrained, it passes on the `.failure` completion down the pipeline. 55 | If the error is that the network is constrained, then the `tryCatch` operator creates a new request to an alternate URL. 56 | 57 | // force a page break - in HTML rendering is just a
58 | <<< 59 | ''' -------------------------------------------------------------------------------- /docs/pattern-continual-error-handling.adoc: -------------------------------------------------------------------------------- 1 | [#patterns-continual-error-handling] 2 | == Using flatMap and catch to handle errors without cancelling the pipeline 3 | 4 | __Goal__:: 5 | 6 | * The `flatMap` operator can be used with `catch` to continue to handle errors on new published values. 7 | 8 | __References__:: 9 | 10 | * <> 11 | * <> 12 | * <> 13 | 14 | __See also__:: 15 | 16 | * <> 17 | * <> 18 | 19 | __Code and explanation__:: 20 | 21 | The `flatMap` operator is the operator to use in handling errors on a continual flow of events. 22 | 23 | You provide a closure to `flatMap` that can read in the value that was provided, and creates a one-shot publisher that does the possibly failing work. 24 | An example of this is requesting data from a network and then decoding the returned data. 25 | You can include a <> operator to capture any errors and provide an appropriate value. 26 | 27 | This is a perfect mechanism for when you want to maintain updates up an upstream publisher, as it creates one-shot publisher or short pipelines that send a single value and then complete for every incoming value. 28 | The completion from the created one-shot publishers terminates in the flatMap and is not passed to downstream subscribers. 29 | 30 | An example of this with a `dataTaskPublisher`: 31 | 32 | [source, swift] 33 | ---- 34 | let remoteDataPublisher = Just(self.testURL!) <1> 35 | .flatMap { url in <2> 36 | URLSession.shared.dataTaskPublisher(for: url) <3> 37 | .tryMap { data, response -> Data in <4> 38 | guard let httpResponse = response as? HTTPURLResponse, 39 | httpResponse.statusCode == 200 else { 40 | throw TestFailureCondition.invalidServerResponse 41 | } 42 | return data 43 | } 44 | .decode(type: PostmanEchoTimeStampCheckResponse.self, decoder: JSONDecoder()) <5> 45 | .catch {_ in <6> 46 | return Just(PostmanEchoTimeStampCheckResponse(valid: false)) 47 | } 48 | } 49 | .eraseToAnyPublisher() 50 | ---- 51 | 52 | <1> `Just` starts this publisher as an example by passing in a URL. 53 | <2> `flatMap` takes the URL as input and the closure goes on to create a one-shot publisher pipeline. 54 | <3> `dataTaskPublisher` uses the input url to make the request. 55 | <4> The result output ( a tuple of `(Data, URLResponse)` ) flows into `tryMap` to be parsed for additional errors. 56 | <5> `decode` attempts to refine the returned data into a locally defined type. 57 | <6> If any of this has failed, `catch` will convert the error into a placeholder sample. 58 | In this case an object with a preset `valid = false` property. 59 | 60 | // force a page break - in HTML rendering is just a
61 | <<< 62 | ''' -------------------------------------------------------------------------------- /docs/pattern-datataskpublisher-decode.adoc: -------------------------------------------------------------------------------- 1 | [#patterns-datataskpublisher-decode] 2 | == Making a network request with dataTaskPublisher 3 | 4 | __Goal__:: 5 | 6 | * One common use case is requesting JSON data from a URL and decoding it. 7 | 8 | __References__:: 9 | 10 | * <> 11 | * <> 12 | * <> 13 | * <> 14 | * <> 15 | 16 | __See also__:: 17 | 18 | * <> 19 | * <> 20 | * <> 21 | 22 | __Code and explanation__:: 23 | 24 | This can be readily accomplished with Combine using 25 | <> followed by a series of operators that process the 26 | data. 27 | Minimally, https://developer.apple.com/documentation/foundation/urlsession/3329708-datataskpublisher[dataTaskPublisher] on https://developer.apple.com/documentation/foundation/urlsession[URLSession] 28 | uses <> and <> before going to the subscriber. 29 | 30 | The simplest case of using this might be: 31 | 32 | [source, swift] 33 | ---- 34 | let myURL = URL(string: "https://postman-echo.com/time/valid?timestamp=2016-10-10") 35 | // checks the validity of a timestamp - this one returns {"valid":true} 36 | // matching the data structure returned from https://postman-echo.com/time/valid 37 | fileprivate struct PostmanEchoTimeStampCheckResponse: Decodable, Hashable { <1> 38 | let valid: Bool 39 | } 40 | 41 | let remoteDataPublisher = URLSession.shared.dataTaskPublisher(for: myURL!) <2> 42 | // the dataTaskPublisher output combination is (data: Data, response: URLResponse) 43 | .map { $0.data } <3> 44 | .decode(type: PostmanEchoTimeStampCheckResponse.self, decoder: JSONDecoder()) <4> 45 | 46 | let cancellableSink = remoteDataPublisher 47 | .sink(receiveCompletion: { completion in 48 | print(".sink() received the completion", String(describing: completion)) 49 | switch completion { 50 | case .finished: <5> 51 | break 52 | case .failure(let anError): <6> 53 | print("received error: ", anError) 54 | } 55 | }, receiveValue: { someValue in <7> 56 | print(".sink() received \(someValue)") 57 | }) 58 | ---- 59 | 60 | <1> Commonly you will have a struct defined that supports at least https://developer.apple.com/documentation/swift/decodable[Decodable] (if not the full https://developer.apple.com/documentation/swift/codable[Codable protocol]). This struct can be defined to only pull the pieces you are interested in from the JSON provided over the network. 61 | The complete JSON payload does not need to be defined. 62 | <2> `dataTaskPublisher` is instantiated from `URLSession`. You can configure your own options on `URLSession`, or use a shared session. 63 | <3> The data that is returned is a tuple: `(data: Data, response: URLResponse)`. 64 | The <> operator is used to get the data and drops the `URLResponse`, returning just `Data` down the pipeline. 65 | <4> <> is used to load the data and attempt to parse it. 66 | Decode can throw an error itself if the decode fails. 67 | If it succeeds, the object passed down the pipeline will be the struct from the JSON data. 68 | <5> If the decoding completed without errors, the finished completion will be triggered and the value will be passed to the `receiveValue` closure. 69 | <6> If the a failure happens (either with the original network request or the decoding), the error will be passed into with the `failure` closure. 70 | <7> Only if the data succeeded with request and decoding will this closure get invoked, and the data format received will be an instance of the struct `PostmanEchoTimeStampCheckResponse`. 71 | 72 | 73 | // force a page break - in HTML rendering is just a
74 | <<< 75 | ''' 76 | -------------------------------------------------------------------------------- /docs/pattern-debugging-pipelines-breakpoint.adoc: -------------------------------------------------------------------------------- 1 | [#patterns-debugging-breakpoint] 2 | == Debugging pipelines with the debugger 3 | 4 | __Goal__:: 5 | 6 | * To force the pipeline to trap into a debugger on specific scenarios or conditions. 7 | 8 | __References__:: 9 | 10 | * <> 11 | * <> 12 | 13 | __See also__:: 14 | 15 | * <> 16 | * <> 17 | 18 | __Code and explanation__:: 19 | 20 | You can set a breakpoint within any closure to any operator within a pipeline, triggering the debugger to activate to inspect the data. 21 | Since the <> operator is frequently used for simple output type conversions, it is often an excellent candidate that has a closure you can use. 22 | If you want to see into the control messages, then a breakpoint within any of the closures provided to <> makes a very convenient target. 23 | 24 | You can also use the <> operator to trigger the debugger, which can be a very quick and convenient way to see what is happening in a pipeline. 25 | The breakpoint operator acts very much like handleEvents, taking a number of optional parameters, closures that are expected to return a boolean, and if true will invoke the debugger. 26 | 27 | The optional closures include: 28 | 29 | * `receiveSubscription` 30 | * `receiveOutput` 31 | * `receiveCompletion` 32 | 33 | [source, swift] 34 | ---- 35 | .breakpoint(receiveSubscription: { subscription in 36 | return false // return true to throw SIGTRAP and invoke the debugger 37 | }, receiveOutput: { value in 38 | return false // return true to throw SIGTRAP and invoke the debugger 39 | }, receiveCompletion: { completion in 40 | return false // return true to throw SIGTRAP and invoke the debugger 41 | }) 42 | ---- 43 | 44 | This allows you to provide logic to evaluate the data being passed through, and only triggering a breakpoint when your specific conditions are met. 45 | With very active pipelines processing a lot of data, this can be a great tool to be more surgical in getting the debugger active when you need it, and letting the other data move on by. 46 | 47 | If you are only interested in the breaking into the debugger on error conditions, then convenience operator <> is perfect. 48 | It takes no parameters or closures, simply invoking the debugger when an error condition of any form is passed through the pipeline. 49 | 50 | [source, swift] 51 | ---- 52 | .breakpointOnError() 53 | ---- 54 | 55 | 56 | [NOTE] 57 | ==== 58 | The location of the breakpoint that is triggered by the breakpoint operator isn't in your code, so getting to local frames and information can be a bit tricky. 59 | This does allow you to inspect global application state in highly specific instances (whenever the closure returns `true`, with logic you provide), but you may find it more effective to use regular breakpoints within closures. 60 | The breakpoint() and breakpointOnError() operators don't immediately drop you into a closure where you can see the data being passed, error thrown, or control signals that may have triggered the breakpoint. 61 | You can often walk back up the stack trace within the debugging window to see the publisher. 62 | 63 | When you trigger a breakpoint within an operator's closure, the debugger immediately gets the context of that closure as well, so you can see/inspect the data being passed. 64 | ==== 65 | 66 | // force a page break - in HTML rendering is just a
67 | <<< 68 | ''' 69 | -------------------------------------------------------------------------------- /docs/pattern-future.adoc: -------------------------------------------------------------------------------- 1 | [#patterns-future] 2 | == Wrapping an asynchronous call with a Future to create a one-shot publisher 3 | 4 | __Goal__:: 5 | 6 | * Using `Future` to turn an asynchronous call into publisher to use the result in a Combine pipeline. 7 | 8 | __References__:: 9 | 10 | * <> 11 | 12 | __See also__:: 13 | 14 | * <> 15 | 16 | __Code and explanation__:: 17 | 18 | [source, swift] 19 | ---- 20 | import Contacts 21 | let futureAsyncPublisher = Future { promise in <1> 22 | CNContactStore().requestAccess(for: .contacts) { grantedAccess, err in <2> 23 | // err is an optional 24 | if let err = err { <3> 25 | return promise(.failure(err)) 26 | } 27 | return promise(.success(grantedAccess)) <4> 28 | } 29 | }.eraseToAnyPublisher() 30 | ---- 31 | 32 | <1> `Future` itself has you define the return types and takes a closure. 33 | It hands in a `Result` object matching the type description, which you interact. 34 | <2> You can invoke the async API however is relevant, including passing in its required closure. 35 | <3> Within the completion handler, you determine what would cause a failure or a success. 36 | A call to `promise(.failure())` returns the failure. 37 | <4> Or a call to `promise(.success())` returns a value. 38 | 39 | [NOTE] 40 | ==== 41 | A <> immediately calls the enclosed asynchronous API call when it is created, *not* when it receives a subscription demand. 42 | This may not be the behavior you want or need. 43 | If you want the call to be bound to subscribers requesting data, you probably want to wrap the Future with <>. 44 | ==== 45 | 46 | If you want to return a resolved promise as a `Future` publisher, you can do so by immediately returning the result you desire its closure. 47 | 48 | The following example returns a single value as a success, with a boolean `true` value. 49 | You could just as easily return `false`, and the publisher would still act as a successful promise. 50 | 51 | [source, swift] 52 | ---- 53 | let resolvedSuccessAsPublisher = Future { promise in 54 | promise(.success(true)) 55 | }.eraseToAnyPublisher() 56 | ---- 57 | 58 | An example of returning a `Future` publisher that immediately resolves as an error: 59 | 60 | [source, swift] 61 | ---- 62 | enum ExampleFailure: Error { 63 | case oneCase 64 | } 65 | 66 | let resolvedFailureAsPublisher = Future { promise in 67 | promise(.failure(ExampleFailure.oneCase)) 68 | }.eraseToAnyPublisher() 69 | ---- 70 | 71 | // force a page break - in HTML rendering is just a
72 | <<< 73 | ''' 74 | -------------------------------------------------------------------------------- /docs/pattern-oneshot-error-handling.adoc: -------------------------------------------------------------------------------- 1 | [#patterns-oneshot-error-handling] 2 | == Using catch to handle errors in a one-shot pipeline 3 | 4 | __Goal__:: 5 | 6 | * If you need to handle a failure within a pipeline, for example before using the `assign` operator or another operator that requires the failure type to be ``, you can use `catch` to provide the appropriate logic. 7 | 8 | __References__:: 9 | 10 | * <> 11 | * <> 12 | 13 | __See also__:: 14 | 15 | * <> 16 | * <> 17 | * <> 18 | 19 | __Code and explanation__:: 20 | 21 | <> handles errors by replacing the upstream publisher with another publisher that you provide as a return in a closure. 22 | 23 | [WARNING] 24 | ==== 25 | Be aware that this effectively terminates the pipeline. 26 | If you're using a one-shot publisher (one that doesn't create more than a single event), then this is fine. 27 | ==== 28 | 29 | For example, <> is a one-shot publisher and you might use catch with it to ensure that you get a response, returning a placeholder in the event of an error. 30 | Extending our previous example to provide a default response: 31 | 32 | [source, swift] 33 | ---- 34 | struct IPInfo: Codable { 35 | // matching the data structure returned from ip.jsontest.com 36 | var ip: String 37 | } 38 | let myURL = URL(string: "http://ip.jsontest.com") 39 | // NOTE(heckj): you'll need to enable insecure downloads in your Info.plist for this example 40 | // since the URL scheme is 'http' 41 | 42 | let remoteDataPublisher = URLSession.shared.dataTaskPublisher(for: myURL!) 43 | // the dataTaskPublisher output combination is (data: Data, response: URLResponse) 44 | .map({ (inputTuple) -> Data in 45 | return inputTuple.data 46 | }) 47 | .decode(type: IPInfo.self, decoder: JSONDecoder()) <1> 48 | .catch { err in <2> 49 | return Publishers.Just(IPInfo(ip: "8.8.8.8"))<3> 50 | } 51 | .eraseToAnyPublisher() 52 | ---- 53 | 54 | <1> Often, a catch operator will be placed after several operators that could fail, in order to provide a fallback or placeholder in the event that any of the possible previous operations failed. 55 | <2> When using catch, you get the error type in and can inspect it to choose how you provide a response. 56 | <3> The Just publisher is frequently used to either start another one-shot pipeline or to directly provide a placeholder response in the event of failure. 57 | 58 | A possible problem with this technique is that the if the original publisher generates more values to which you wish to react, the original pipeline has been ended. 59 | If you are creating a pipeline that reacts to a <> property, then after any failed value that activates the catch operator, the pipeline will cease to react further. 60 | See <> for more details of how this works. 61 | 62 | If you want to continue to respond to errors and handle them, see the pattern <>. 63 | 64 | // force a page break - in HTML rendering is just a
65 | <<< 66 | ''' 67 | -------------------------------------------------------------------------------- /docs/pattern-retry.adoc: -------------------------------------------------------------------------------- 1 | [#patterns-retry] 2 | == Retrying in the event of a temporary failure 3 | 4 | __Goal__:: 5 | 6 | * The <> operator can be included in a pipeline to retry a subscription when a `.failure` completion occurs. 7 | 8 | __References__:: 9 | 10 | * <> 11 | * <> 12 | * <> 13 | * <> 14 | 15 | __See also__:: 16 | 17 | * <> 18 | * <> 19 | 20 | __Code and explanation__:: 21 | 22 | When requesting data from a `dataTaskPublisher`, the request may fail. 23 | In that case you will receive a `.failure` completion with an error. 24 | When it fails, the <> operator will let you retry that same request for a set number of attempts. 25 | The `retry` operator passes through the resulting values when the publisher does not send a `.failure` completion. 26 | `retry` only reacts within a combine pipeline when a `.failure` completion is sent. 27 | 28 | When `retry` receives a `.failure` completion, the way it retries is by recreating the subscription to the operator or publisher to which it was chained. 29 | 30 | The <> operator is commonly desired when attempting to request network resources with an unstable connection, or other situations where the request might succeed if the request happens again. 31 | If the number of retries specified all fail, then the `.failure` completion is passed down to the subscriber. 32 | 33 | In our example below, we are using retry in combination with a <> operator. 34 | Our use of the delay operator puts a small random delay before the next request. 35 | This spaces out the retry attempts, so that the retries do not happen in quick succession. 36 | 37 | This example also includes the use of the <> operator to more fully inspect any URLResponse returned from the `dataTaskPublisher`. 38 | Any response from the server is encapsulated by `URLSession`, and passed forward as a valid response. 39 | `URLSession` does not treat a _404 Not Found_ http response as an error response, nor any of the _50x_ error codes. 40 | Using `tryMap` lets us inspect the response code that was sent, and verify that it was a 200 response code. 41 | In this example, if the response code is anything but a 200 response, it throws an exception - which in turn causes the tryMap operator to pass down a `.failure` completion rather than data. 42 | This example sets the `tryMap` *after* the retry operator so that retry will only re-attempt the request when the site didn't respond. 43 | 44 | [source, swift] 45 | ---- 46 | let remoteDataPublisher = urlSession.dataTaskPublisher(for: self.URL!) 47 | .delay(for: DispatchQueue.SchedulerTimeType.Stride(integerLiteral: Int.random(in: 1..<5)), scheduler: backgroundQueue) <1> 48 | .retry(3) <2> 49 | .tryMap { data, response -> Data in <3> 50 | guard let httpResponse = response as? HTTPURLResponse, 51 | httpResponse.statusCode == 200 else { 52 | throw TestFailureCondition.invalidServerResponse 53 | } 54 | return data 55 | } 56 | .decode(type: PostmanEchoTimeStampCheckResponse.self, decoder: JSONDecoder()) 57 | .subscribe(on: backgroundQueue) 58 | .eraseToAnyPublisher() 59 | ---- 60 | 61 | <1> The <> operator will hold the results flowing through the pipeline for a short duration, in this case for a random selection of 1 to 5 seconds. By adding delay here in the pipeline, it will always occur, even if the original request is successful. 62 | <2> Retry is specified as trying 3 times. 63 | This will result in a total of four attempts if each fails - the original request and 3 additional attempts. 64 | <3> tryMap is being used to inspect the data result from dataTaskPublisher and return a `.failure` completion if the response from the server is valid, but not a 200 HTTP response code. 65 | 66 | [WARNING] 67 | ==== 68 | When using the <> operator with <>, verify that the URL you are requesting isn't going to have negative side effects if requested repeatedly or with a retry. 69 | Ideally such requests are be expected to be idempotent. 70 | If they are not, the <> operator may make multiple requests, with very unexpected side effects. 71 | ==== 72 | 73 | // force a page break - in HTML rendering is just a
74 | <<< 75 | ''' 76 | -------------------------------------------------------------------------------- /docs/pattern-sink.adoc: -------------------------------------------------------------------------------- 1 | [#patterns-sink-subscriber] 2 | == Creating a subscriber with sink 3 | 4 | __Goal__:: 5 | 6 | * To receive the output, and the errors or completion messages, generated from a publisher or through a pipeline, you can create a subscriber with <>. 7 | 8 | __References__:: 9 | 10 | * <> 11 | 12 | __See also__:: 13 | 14 | * <> 15 | * <> 16 | * <> 17 | 18 | __Code and explanation__:: 19 | 20 | Sink creates an all-purpose subscriber to capture or react to the data from a Combine pipeline, while also supporting cancellation and the <>. 21 | 22 | .simple sink 23 | [source, swift] 24 | ---- 25 | let cancellablePipeline = publishingSource.sink { someValue in <1> 26 | // do what you want with the resulting value passed down 27 | // be aware that depending on the publisher, this closure 28 | // may be invoked multiple times. 29 | print(".sink() received \(someValue)") 30 | }) 31 | ---- 32 | <1> The simple version of a sink is very compact, with a single trailing closure receiving data when presented through the pipeline. 33 | 34 | .sink with completions and data 35 | [source, swift] 36 | ---- 37 | let cancellablePipeline = publishingSource.sink(receiveCompletion: { completion in <1> 38 | switch completion { 39 | case .finished: 40 | // no associated data, but you can react to knowing the 41 | // request has been completed 42 | break 43 | case .failure(let anError): 44 | // do what you want with the error details, presenting, 45 | // logging, or hiding as appropriate 46 | print("received the error: ", anError) 47 | break 48 | } 49 | }, receiveValue: { someValue in 50 | // do what you want with the resulting value passed down 51 | // be aware that depending on the publisher, this closure 52 | // may be invoked multiple times. 53 | print(".sink() received \(someValue)") 54 | }) 55 | 56 | cancellablePipeline.cancel() <2> 57 | ---- 58 | 59 | <1> Sinks are created by chaining the code from a publisher or pipeline, and provide an end point for the pipeline. 60 | When the sink is created or invoked on a publisher, it implicitly starts <> with the `subscribe` method, requesting unlimited data. 61 | <2> Sinks are cancellable subscribers. At any time you can take the reference that terminated with sink and invoke `.cancel()` on it to invalidate and shut down the pipeline. 62 | 63 | // force a page break - in HTML rendering is just a
64 | <<< 65 | ''' 66 | -------------------------------------------------------------------------------- /docs/pattern-template.adoc: -------------------------------------------------------------------------------- 1 | [#patterns-someName] 2 | == Pattern N: summary description 3 | 4 | __Goal__:: 5 | 6 | * what the pattern does and why we want to use it 7 | 8 | __References__:: 9 | 10 | * << link to reference pages>> 11 | 12 | __See also__:: 13 | 14 | * << link to other patterns>> 15 | 16 | explanation w/ code 17 | 18 | // force a page break - in HTML rendering is just a
19 | <<< 20 | -------------------------------------------------------------------------------- /docs/raw-notes.adoc: -------------------------------------------------------------------------------- 1 | [#raw-notes] 2 | = Raw Notes 3 | 4 | Words and phrases to look for and destroy: 5 | 6 | "simply" 7 | "just" 8 | "and then" 9 | "are then" 10 | "you should" 11 | 12 | Look for an verify: 13 | 14 | "stream" vs pipeline 15 | 16 | Things to verify are consistent: 17 | 18 | `finished` -> `.finished` 19 | `failure` -> `.failure` 20 | 21 | `.failure` completion 22 | `.finished` completion -------------------------------------------------------------------------------- /docs/reference-template.adoc: -------------------------------------------------------------------------------- 1 | __Summary__:: 2 | 3 | n/a 4 | 5 | __Constraints on connected publisher__:: 6 | 7 | * __none__ 8 | 9 | __icon:apple[set=fab] docs__:: n/a 10 | 11 | __Usage__:: 12 | 13 | n/a 14 | 15 | __Details__:: 16 | 17 | n/a -------------------------------------------------------------------------------- /docs/using-combine-book.adoc: -------------------------------------------------------------------------------- 1 | = Using Combine 2 | Joseph Heck 3 | v 1.2.2, 2022-05-24 4 | :doctype: book 5 | :creator: {author} 6 | :producer: Joseph Heck 7 | :keywords: Apple, Combine, ReactiveX, SwiftUI 8 | :copyright: Joseph Heck 2019-2022 9 | :publication-type: book 10 | // NOTE use 'anthology' for per-chapter author support 11 | :idprefix: 12 | :idseparator: - 13 | :imagesdir: images 14 | :front-cover-image: image:UsingCombineWithSwiftCoverArt_2560x1920.png[fit=cover] 15 | :google-analytics-account: UA-898243-5 16 | :source-highlighter: rouge 17 | :url-issues: https://github.com/heckj/swiftui-notes/issues 18 | :toc: left 19 | :toclevels: 4 20 | // enable font-awesome icons in content 21 | :icons: font 22 | 23 | ifndef::ebook-format[:leveloffset: 1] 24 | 25 | include::aboutthisbook.adoc[] 26 | include::introduction.adoc[] 27 | include::coreconcepts.adoc[] 28 | include::developingwith.adoc[] 29 | include::patterns.adoc[] 30 | include::reference.adoc[] 31 | -------------------------------------------------------------------------------- /docs_zh-CN/aboutthisbook.adoc: -------------------------------------------------------------------------------- 1 | [#aboutthisbook] 2 | = 关于本书 3 | 4 | ifeval::["{backend}" == "html5"] 5 | (link:./index.html[english]) (link:./index_zh-CN.html[普通话]) 6 | endif::[] 7 | 8 | 版本号: {revnumber} 9 | 10 | 该版日期: {revdate} 11 | 12 | 这是一本中高级难度的书,主要关注在如何使用 Combine 框架。 13 | 你需要对 Swift 及其中的引用和值类型、协议有透彻的理解,并且能够熟练使用 Foundation 框架中的常用元素,才能阅读本书和其中的示例。 14 | 15 | 如果你刚开始学习 Swift, https://developer.apple.com/swift/resources/[Apple 提供了一些资源] 可以用来学习, 16 | 还有一些作者写了非常棒的教程和入门书籍, 例如 Daniel Steinberg 写的 https://gumroad.com/l/swift-kickstart[A Swift Kickstart] 和 Paul Hudson 写的 https://www.hackingwithswift.com[Hacking with Swift]。 17 | 18 | 这本书提供了对函数响应式编程概念的 <>, 这正是 Combine 所要提供的编程方式。 19 | 20 | == 支持作者 21 | 22 | **_如果您觉得内容有用,可购买没有数字版权管理英文原版的 PDF 或 ePub 版本 http://gumroad.com/l/usingcombine._** 23 | 24 | 这本书提供免费的 https://heckj.github.io/swiftui-notes/[线上英文原版] 和 https://zhiying.space/using-combine/[中文翻译版]。 25 | 26 | 如果发现中文翻译版有拼写、语法或者技术错误想要指出,可以 fork 这个仓库,更新或者纠正之后创建一个 https://github.com/zhiying-fan/using-combine/pulls[pull requests] 给我。 27 | 28 | 如果发现英文原版有拼写、语法或者技术错误想要指出,请在 GitHub https://github.com/heckj/swiftui-notes/issues/new/choose[新建一个 issue]。 29 | 如果你愿意的话,也可以 fork 英文原版的仓库,更新或者纠正之后创建一个 https://github.com/heckj/swiftui-notes/compare?expand=1[pull requests] 给作者。 30 | 31 | == 致谢 32 | 33 | .感谢 34 | **** 35 | Michael Critz 设计并提供封面。 36 | 37 | 以下人员的检查、指正和更新: 38 | 39 | Benjamin Barnard, 40 | Mycroft Canner, 41 | Max Desiatov, 42 | Tim Ekl, 43 | Malcolm Hall, 44 | Arthur Hammer, 45 | Nanu Jogi, 46 | Serhii Kyrylenko, 47 | Brett Markowitz, 48 | Matt Massicotte, 49 | Michel Mohrmann, 50 | John Mueller, 51 | Lee O'Mara, 52 | Kai Özer, 53 | Martin Pfundmair, 54 | Zachary Recolan, 55 | Dave Reed, 56 | Dean Scarff, 57 | Andrius Shiaulis, 58 | Antoine Weber, 59 | Paul Wood III, 60 | Federico Zanetello 61 | 62 | 中文版翻译: 63 | 樊志颖, 64 | 卫林霄 65 | **** 66 | 67 | 谢谢你们所有人花费时间和精力提交 pull request,使这本书变得更好! 68 | 69 | == 作者简介 70 | 71 | Joe Heck 在初创公司和大型公司中拥有广泛的软件工程开发和管理经验。 72 | 他为架构、开发、验证、部署和操作这所有阶段提供解决方案。 73 | 74 | Joe 开发了从移动和桌面应用程序开发的项目到基于云的分布式系统。 75 | 他建立了团队、开发流程、CI 和 CD 流水线,并制定了验证和运营自动化。 76 | Joe 还指导人们学习、构建、验证、部署和运行软件服务和基础架构。 77 | 78 | Joe 广泛的贡献和参与到各种开源项目的工作中。 79 | 他在网站 https://rhonabwy.com/ 上撰写了各种主题的文章。 80 | 81 | [cols="3*^",frame=none,grid=none,width=50%] 82 | |=== 83 | .^| https://github.com/heckj[icon:github[size=2x,set=fab]] 84 | .^| https://www.linkedin.com/in/josephheck/[icon:linkedin[size=2x,set=fab]] 85 | .^| http://twitter.com/heckj[icon:twitter[size=2x,set=fab]] 86 | |=== 87 | 88 | == 译者简介 89 | 90 | 樊志颖,专注于 iOS 开发。 91 | 92 | 个人网站: https://zhiying.space 93 | 94 | Github: https://github.com/zhiying-fan 95 | 96 | 卫林霄,iOS 开发。 97 | 98 | GitHub: https://github.com/yeland 99 | 100 | == 翻译术语表 101 | 102 | [cols="2*^"] 103 | |=== 104 | | Framework 105 | | 框架 106 | 107 | | Pipeline 108 | | 管道 109 | 110 | | Functional programming 111 | | 函数式编程 112 | 113 | | Functional reactive programming 114 | | 函数响应式编程 115 | 116 | | Publisher 117 | | 发布者 118 | 119 | | Subscriber 120 | | 订阅者 121 | 122 | | Operator 123 | | 操作符 124 | 125 | |=== 126 | 127 | == 从哪获取这本书 128 | 129 | 本书的线上版本以 HTML 的形式免费提供, https://heckj.github.io/swiftui-notes/[英文原版] 和 https://zhiying.space/using-combine/[中文翻译版]。 130 | 131 | 没有数字版权管理英文原版的 PDF 或 ePub 版本可以在 http://gumroad.com/l/usingcombine 购买。 132 | 133 | 随着开发的继续,将对线上版本的内容持续更新。 134 | 更大的更新和宣告也会通过 https://gumroad.com/heckj[作者在 Gumroad 的简介] 进行提供。 135 | 136 | 本书的内容包括示例代码和测试,都放在 GitHub 的仓库中: https://github.com/heckj/swiftui-notes 。 137 | 138 | === 下载项目 139 | 140 | 本书的内容以及本书引用的示例代码和单元测试,都被链接到了一个 Xcode 的项目中(`swiftui-notes.xcodeproj`)。 141 | 该 Xcode 项目包括完全可实操的示例代码,展示了 Combine 与 Uikit 和 SwiftUI 集成的示例。 142 | 该项目还包括运用此框架的大量单元测试,以说明框架组件的行为。 143 | 144 | 与本书关联的项目需要 Xcode 11 和 Macos 10.14 或更高版本。 145 | 146 | image::welcomeToXcode.png[Welcome to Xcode,406,388] 147 | 148 | * 从 Welcome to Xcode 窗口,选择 **Clone an existing project** 149 | * 输入 `https://github.com/heckj/swiftui-notes.git` 然后点击 `Clone` 150 | 151 | image::cloneRepository.png[clone Repository,463,263] 152 | 153 | * 选择 `master` 分支检出 154 | 155 | // force a page break - ignored in HTML rendering 156 | <<< 157 | -------------------------------------------------------------------------------- /docs_zh-CN/pattern-assertnofailure.adoc: -------------------------------------------------------------------------------- 1 | [#patterns-assertnofailure] 2 | == 使用 assertNoFailure 验证未发生失败 3 | 4 | __目的__:: 5 | 6 | * 验证管道内未发生错误 7 | 8 | __参考__:: 9 | 10 | * <> 11 | 12 | __另请参阅__:: 13 | 14 | * <> 15 | * <> 16 | 17 | __代码和解释__:: 18 | 19 | 在管道中测试常量时,断言 assertNoFailure 非常有用,可将失败类型转换为 ``。 20 | 如果断言被触发,该操作符将导致应用程序终止(或测试时导致调试器崩溃)。 21 | 22 | 这对于验证已经处理过错误的常量很有用。 23 | 比如你确信你处理了错误,对管道进行了 map 操作,该操作可以将 `` 的失败类型转换为 `` 传给所需的订阅者。 24 | 25 | 更有可能的是,你希望将错误处理掉,而不是终止应用程序。 26 | 期待后面的 <> 和 <> 模式吧,它们会告诉你如何提供逻辑来处理管道中的错误。 27 | 28 | // force a page break - in HTML rendering is just a
29 | <<< 30 | ''' 31 | -------------------------------------------------------------------------------- /docs_zh-CN/pattern-assign.adoc: -------------------------------------------------------------------------------- 1 | [#patterns-assign-subscriber] 2 | == 使用 assign 创建一个订阅者 3 | 4 | __目的__:: 5 | 6 | * 使用管道的结果来设置值,这个值通常是位于用户界面或控制组件上的属性,不过任何符合 KVO 的对象都可以提供该值。 7 | 8 | __参考__:: 9 | 10 | * <> 11 | * <> 12 | 13 | __另请参阅__:: 14 | 15 | * <> 16 | 17 | __代码和解释__:: 18 | 19 | Assign 是专门设计用于将来自发布者或管道的数据应用到属性的订阅者,每当它收到数据时都会更新该属性。 20 | 与 sink 一样,它创建时激活并请求无限数据。 21 | Assign 要求将失败类型指定为 ``,因此,如果你的管道可能失败(例如使用 tryMap 等操作符),则需要在使用 `.assign` 之前 <>。 22 | 23 | .简单的 assign 例子 24 | [source, swift] 25 | ---- 26 | let cancellablePipeline = publishingSource <1> 27 | .receive(on: RunLoop.main) <2> 28 | .assign(to: \.isEnabled, on: yourButton) <3> 29 | 30 | cancellablePipeline.cancel() <4> 31 | ---- 32 | 33 | <1> `.assign` 通常在创建时链接到发布者,并且返回值是可取消的。 34 | <2> 如果 `.assign` 被用于更新用户界面的元素,则需要确保在主线程更新它。这个调用确保了订阅者是在主线程上接收数据的。 35 | <3> Assign 持有对使用 https://developer.apple.com/documentation/swift/referencewritablekeypath[key path] 更新的属性的引用,以及对正在更新的对象的引用。 36 | <4> 在任何时候,你都可以调用 `cancel()` 终止和使管道失效。通常,当把从管道中更新的对象(如 viewController)销毁时,我们会取消管道。 37 | 38 | // force a page break - in HTML rendering is just a
39 | <<< 40 | ''' 41 | -------------------------------------------------------------------------------- /docs_zh-CN/pattern-constrained-network.adoc: -------------------------------------------------------------------------------- 1 | [#patterns-constrained-network] 2 | == 网络受限时从备用 URL 请求数据 3 | 4 | __目的__:: 5 | 6 | * 在 Apple 的 WWDC 2019 演示 https://developer.apple.com/videos/play/wwdc2019/712/[Advances in Networking, Part 1] 中,使用 `tryCatch` 和 `tryMap` 操作符提供了示例模式,以响应网络受到限制的特殊错误。 7 | 8 | __参考__:: 9 | 10 | * <> 11 | * <> 12 | * <> 13 | 14 | __另请参阅__:: 15 | 16 | * <> 17 | * <> 18 | 19 | __代码和解释__:: 20 | 21 | [source, swift] 22 | ---- 23 | // Generalized Publisher for Adaptive URL Loading 24 | func adaptiveLoader(regularURL: URL, lowDataURL: URL) -> AnyPublisher { 25 | var request = URLRequest(url: regularURL) <1> 26 | request.allowsConstrainedNetworkAccess = false <2> 27 | return URLSession.shared.dataTaskPublisher(for: request) <3> 28 | .tryCatch { error -> URLSession.DataTaskPublisher in <4> 29 | guard error.networkUnavailableReason == .constrained else { 30 | throw error 31 | } 32 | return URLSession.shared.dataTaskPublisher(for: lowDataURL) <5> 33 | .tryMap { data, response -> Data in 34 | guard let httpResponse = response as? HTTPUrlResponse, <6> 35 | httpResponse.statusCode == 200 else { 36 | throw MyNetworkingError.invalidServerResponse 37 | } 38 | return data 39 | } 40 | .eraseToAnyPublisher() <7> 41 | ---- 42 | 43 | 在苹果的 WWDC 中的这个例子,提供了一个函数,接受两个 URL 作为参数 —— 一个主要的 URL 和一个备用的。 44 | 它会返回一个发布者,该发布者将请求数据,并在网络受到限制时向备用 URL 请求数据。 45 | 46 | <1> request 变量是一个尝试请求数据的 `URLRequest`。 47 | <2> 设置 `request.allowsConstrainedNetworkAccess` 将导致 `dataTaskPublisher` 在网络受限时返回错误。 48 | <3> 调用 `dataTaskPublisher` 发起请求。 49 | <4> `tryCatch` 用于捕获当前的错误状态并检查特定错误(受限的网络)。 50 | <5> 如果它发现错误,它会使用备用 URL 创建一个新的一次性发布者。 51 | <6> 由此产生的发布者仍可能失败,`tryMap` 可以基于对应到错误条件的 HTTP 响应码来抛出错误,将此映射为失败。 52 | <7> `eraseToAnyPublisher` 可在操作符链上进行类型擦除,因此 adaptiveLoader 函数的返回类型为 `AnyPublisher`。 53 | 54 | 在示例中,如果从原始请求返回的错误不是网络受限的问题,则它会将 `.failure` 结束事件传到管道中。 55 | 如果错误是网络受限,则 `tryCatch` 操作符会创建对备用 URL 的新请求。 56 | 57 | // force a page break - in HTML rendering is just a
58 | <<< 59 | ''' -------------------------------------------------------------------------------- /docs_zh-CN/pattern-continual-error-handling.adoc: -------------------------------------------------------------------------------- 1 | [#patterns-continual-error-handling] 2 | == 使用 flatMap 和 catch 在不取消管道的情况下处理错误 3 | 4 | __目的__:: 5 | 6 | * `flatMap` 操作符可以与 `catch` 一起使用,以持续处理新发布的值上的错误。 7 | 8 | __参考__:: 9 | 10 | * <> 11 | * <> 12 | * <> 13 | 14 | __另请参阅__:: 15 | 16 | * <> 17 | * <> 18 | 19 | __代码和解释__:: 20 | 21 | `flatMap` 是用于处理持续事件流中错误的操作符。 22 | 23 | 你提供一个闭包给 `flatMap`,该闭包可以获取所传入的值,并创建一个一次性的发布者,完成可能失败的工作。 24 | 这方面的一个例子是从网络请求数据,然后将其解码。 25 | 你可以引入一个 <> 操作符,以捕获任何错误并提供适当的值。 26 | 27 | 当你想要保持对上游发布者的更新时,这是一个完美的机制,因为它创建一次性的发布者或短管道,发送一个单一的值,然后完成每一个传入的值。 28 | 所创建的一次性发布者的完成事件在 flatMap 中终止,并且不会传递给下游订阅者。 29 | 30 | 一个使用 `dataTaskPublisher` 的这样的例子: 31 | 32 | [source, swift] 33 | ---- 34 | let remoteDataPublisher = Just(self.testURL!) <1> 35 | .flatMap { url in <2> 36 | URLSession.shared.dataTaskPublisher(for: url) <3> 37 | .tryMap { data, response -> Data in <4> 38 | guard let httpResponse = response as? HTTPURLResponse, 39 | httpResponse.statusCode == 200 else { 40 | throw TestFailureCondition.invalidServerResponse 41 | } 42 | return data 43 | } 44 | .decode(type: PostmanEchoTimeStampCheckResponse.self, decoder: JSONDecoder()) <5> 45 | .catch {_ in <6> 46 | return Just(PostmanEchoTimeStampCheckResponse(valid: false)) 47 | } 48 | } 49 | .eraseToAnyPublisher() 50 | ---- 51 | 52 | <1> `Just` 以传入一个 URL 作为示例启动此发布者。 53 | <2> `flatMap` 以 URL 作为输入,闭包继续创建一次性发布者管道。 54 | <3> `dataTaskPublisher` 使用输入的 url 发出请求。 55 | <4> 输出的结果(一个 `(Data, URLResponse)` 元组)流入 `tryMap` 以解析其他错误。 56 | <5> `decode` 尝试将返回的数据转换为本地定义的类型。 57 | <6> 如果其中任何一个失败,`catch` 将把错误转换为一个默认的值。 58 | 在这个例子中,是具有预设好 `valid = false` 属性的对象。 59 | 60 | // force a page break - in HTML rendering is just a
61 | <<< 62 | ''' -------------------------------------------------------------------------------- /docs_zh-CN/pattern-datataskpublisher-decode.adoc: -------------------------------------------------------------------------------- 1 | [#patterns-datataskpublisher-decode] 2 | == 使用 dataTaskPublisher 发起网络请求 3 | 4 | __目的__:: 5 | 6 | * 一个常见的用例是从 URL 请求 JSON 数据并解码。 7 | 8 | __参考__:: 9 | 10 | * <> 11 | * <> 12 | * <> 13 | * <> 14 | * <> 15 | 16 | __另请参阅__:: 17 | 18 | * <> 19 | * <> 20 | * <> 21 | 22 | __代码和解释__:: 23 | 24 | 这可以通过使用 Combine 的 <> 搭配一系列处理数据的操作符来轻松完成。 25 | 26 | 27 | 最简单的,调用 https://developer.apple.com/documentation/foundation/urlsession[URLSession] 的 https://developer.apple.com/documentation/foundation/urlsession/3329708-datataskpublisher[dataTaskPublisher],然后在数据到达订阅者之前使用 <> 和 <>。 28 | 29 | 30 | 使用此操作的最简单例子可能是: 31 | 32 | [source, swift] 33 | ---- 34 | let myURL = URL(string: "https://postman-echo.com/time/valid?timestamp=2016-10-10") 35 | // checks the validity of a timestamp - this one returns {"valid":true} 36 | // matching the data structure returned from https://postman-echo.com/time/valid 37 | fileprivate struct PostmanEchoTimeStampCheckResponse: Decodable, Hashable { <1> 38 | let valid: Bool 39 | } 40 | 41 | let remoteDataPublisher = URLSession.shared.dataTaskPublisher(for: myURL!) <2> 42 | // the dataTaskPublisher output combination is (data: Data, response: URLResponse) 43 | .map { $0.data } <3> 44 | .decode(type: PostmanEchoTimeStampCheckResponse.self, decoder: JSONDecoder()) <4> 45 | 46 | let cancellableSink = remoteDataPublisher 47 | .sink(receiveCompletion: { completion in 48 | print(".sink() received the completion", String(describing: completion)) 49 | switch completion { 50 | case .finished: <5> 51 | break 52 | case .failure(let anError): <6> 53 | print("received error: ", anError) 54 | } 55 | }, receiveValue: { someValue in <7> 56 | print(".sink() received \(someValue)") 57 | }) 58 | ---- 59 | 60 | <1> 通常,你将有一个结构体的定义,至少遵循 https://developer.apple.com/documentation/swift/decodable[Decodable] 协议(即使没有完全遵循 https://developer.apple.com/documentation/swift/codable[Codable protocol])。此结构体可以只定义从网络拉取到的 JSON 中你感兴趣的字段。 61 | 不需要定义完整的 JSON 结构。 62 | <2> `dataTaskPublisher` 是从 `URLSession` 实例化的。 你可以配置你自己的 `URLSession`,或者使用 shared session. 63 | <3> 返回的数据是一个元组:`(data: Data, response: URLResponse)`。 64 | <> 操作符用来获取数据并丢弃 `URLResponse`,只把 `Data` 沿管道向下传递。 65 | <4> <> 用于加载数据并尝试解析它。 66 | 如果解码失败,它会抛出一个错误。 67 | 如果它成功,通过管道传递的对象将是来自 JSON 数据的结构体。 68 | <5> 如果解码完成且没有错误,则将触发完成操作,并将值传递给 `receiveValue` 闭包。 69 | <6> 如果发生失败(无论是网络请求还是解码),则错误将被传递到 `failure` 闭包。 70 | <7> 只有当数据请求并解码成功时,才会调用此闭包,并且收到的数据格式将是结构体 `PostmanEchoTimeStampCheckResponse` 的实例。 71 | 72 | 73 | // force a page break - in HTML rendering is just a
74 | <<< 75 | ''' 76 | -------------------------------------------------------------------------------- /docs_zh-CN/pattern-debugging-pipelines-breakpoint.adoc: -------------------------------------------------------------------------------- 1 | [#patterns-debugging-breakpoint] 2 | == 使用调试器调试管道 3 | 4 | __目的__:: 5 | 6 | * 强制管道在特定场景或条件下进入调试器。 7 | 8 | __参考__:: 9 | 10 | * <> 11 | * <> 12 | 13 | __另请参阅__:: 14 | 15 | * <> 16 | * <> 17 | 18 | __代码和解释__:: 19 | 20 | 你可以在管道内的任何操作符的任何闭包内设置一个断点,触发调试器激活以检查数据。 21 | 由于 <> 操作符经常用于简单的输出类型转换,因此它通常是具有你可以使用的闭包的优秀候选者。 22 | 如果你想查看控制消息,那么为 <> 提供的任何闭包添加一个断点,目标实现起来将非常方便。 23 | 24 | 你还可以使用 <> 操作符触发调试器,这是查看管道中发生情况的一种非常快速和方便的方式。 25 | breakpoint 操作符的行为非常像 handleEvents,使用一些可选参数,期望返回一个布尔值的闭包,如果返回 true 将会调用调试器。 26 | 27 | 可选的闭包包括: 28 | 29 | * `receiveSubscription` 30 | * `receiveOutput` 31 | * `receiveCompletion` 32 | 33 | [source, swift] 34 | ---- 35 | .breakpoint(receiveSubscription: { subscription in 36 | return false // return true to throw SIGTRAP and invoke the debugger 37 | }, receiveOutput: { value in 38 | return false // return true to throw SIGTRAP and invoke the debugger 39 | }, receiveCompletion: { completion in 40 | return false // return true to throw SIGTRAP and invoke the debugger 41 | }) 42 | ---- 43 | 44 | 这允许你提供逻辑来评估正在传递的数据,并且仅在满足特定条件时触发断点。 45 | 通过非常活跃的管道会处理大量数据,这将是一个非常有效的工具,在需要调试器时,让调试器处于活动状态,并让其他数据继续移动。 46 | 47 | 如果你只想在错误条件下进入调试器,则便利的操作符 <> 是完美的选择。 48 | 它不需要参数或闭包,当任何形式的错误条件通过管道时,它都会调用调试器。 49 | 50 | [source, swift] 51 | ---- 52 | .breakpointOnError() 53 | ---- 54 | 55 | 56 | [NOTE] 57 | ==== 58 | 断点操作符触发的断点位置不在你的代码中,因此访问本地堆栈和信息可能有点棘手。 59 | 这确实允许你在极其特定的情况下检查全局应用状态(每当闭包返回 `true` 时,使用你提供的逻辑),但你可能会发现在闭包中使用常规断点更有效。 60 | breakpoint() 和 breakpointOnError() 操作符不会立即将你带到闭包的位置,在那里你可以看到可能触发断点的正在传递的数据、抛出的错误或控制信号。 61 | 你通常可以在调试窗口内通过堆栈跟踪以查看发布者。 62 | 63 | 当你在操作符的闭包中触发断点时,调试器也会立即获取该闭包的上下文,以便你可以查看/检查正在传递的数据。 64 | ==== 65 | 66 | // force a page break - in HTML rendering is just a
67 | <<< 68 | ''' 69 | -------------------------------------------------------------------------------- /docs_zh-CN/pattern-debugging-pipelines-handleevents.adoc: -------------------------------------------------------------------------------- 1 | [#patterns-debugging-handleevents] 2 | == 使用 handleEvents 操作符调试管道 3 | 4 | __目的__:: 5 | 6 | * 使用断点、打印、记录语句或其他额外的逻辑,以便更有针对性地了解管道内发生的情况。 7 | 8 | __参考__:: 9 | 10 | * <> 11 | * 使用 handleEvents 的 ViewController 在 github 项目中位于 https://github.com/heckj/swiftui-notes/blob/master/UIKit-Combine/GithubViewController.swift[UIKit-Combine/GithubViewController.swift] 12 | * 有关 handleEvents 的单元测试在 github 项目中位于 https://github.com/heckj/swiftui-notes/blob/master/UsingCombineTests/HandleEventsPublisherTests.swift[UsingCombineTests/HandleEventsPublisherTests.swift] 13 | 14 | __另请参阅__:: 15 | 16 | * <> 17 | * <> 18 | * <> 19 | * <> 20 | * <> 21 | 22 | __代码和解释__:: 23 | 24 | <> 传入数据,不对输出和失败类型或数据进行任何修改。 25 | 当你在管道中加入该操作符时,可以指定一些可选的闭包,从而让你能够专注于你想要看到的信息。 26 | 具有特定闭包的 <> 操作符是一个打开新窗口的好方法,通过该窗口可以查看管道取消、出错或以其他预期的方式终止时发生的情况。 27 | 28 | 可以指定的闭包包括: 29 | 30 | * `receiveSubscription` 31 | * `receiveRequest` 32 | * `receiveCancel` 33 | * `receiveOutput` 34 | * `receiveCompletion` 35 | 36 | 如果每个闭包都包含打印语句,则该操作符将非常像 <> 操作符,具体表现在 <>。 37 | 38 | 使用 handleEvents 调试的强大之处在于可以选择要查看的内容、减少输出量或操作数据以更好地了解它。 39 | 40 | 在 https://github.com/heckj/swiftui-notes/blob/master/UIKit-Combine/GithubViewController.swift[UIKit-Combine/GithubViewController.swift] 的示例 viewcontroller 中,订阅、取消和 completion 的事件被用于启动或停止 UIActivityIndicatorView。 41 | 42 | 如果你只想看到管道上传递的数据,而不关心控制消息,那么为 `receiveOutput` 提供单个闭包并忽略其他闭包可以让你专注于这些详细信息。 43 | 44 | handleEvents 的单元测试示例展示了所有可提供的闭包: 45 | 46 | .https://github.com/heckj/swiftui-notes/blob/master/UsingCombineTests/HandleEventsPublisherTests.swift[UsingCombineTests/HandleEventsPublisherTests.swift] 47 | [source, swift] 48 | ---- 49 | .handleEvents(receiveSubscription: { aValue in 50 | print("receiveSubscription event called with \(String(describing: aValue))") <2> 51 | }, receiveOutput: { aValue in <3> 52 | print("receiveOutput was invoked with \(String(describing: aValue))") 53 | }, receiveCompletion: { aValue in <4> 54 | print("receiveCompletion event called with \(String(describing: aValue))") 55 | }, receiveCancel: { <5> 56 | print("receiveCancel event invoked") 57 | }, receiveRequest: { aValue in <1> 58 | print("receiveRequest event called with \(String(describing: aValue))") 59 | }) 60 | ---- 61 | <1> 第一个被调用的闭包是 `receiveRequest`,所需要的值(the demand value)将传递给它。 62 | <2> 第二个闭包 `receiveSubscription` 通常是从发布者返回的订阅消息,它将对订阅的引用传递给发布者。 63 | 此时,管道已运行,发布者将根据原始请求中请求的数据量提供数据。 64 | <3> 当发布者提供这些数据时,这些数据将传递到 `receiveOutput` 中,每次有值传递过来都将调用该闭包。 65 | 这将随着发布者发送更多的值而重复调用。 66 | <4> 如果管道正常关闭或因失败而终止,`receiveCompletion` 闭包将收到 completion 事件。 67 | 就像 <> 闭包一样,你可以对提供的 completion 事件使用 switch,如果它是一个 `.failure` completion,那么你可以检查附带的错误。 68 | <5> 如果管道被取消,则将调用 `receiveCancel` 闭包。 69 | 不会有任何数据传递到该取消闭包中。 70 | 71 | [NOTE] 72 | ==== 73 | 虽然你还可以使用 <> 和 <> 操作符进入调试模式(如<> 中所示),带有闭包的 `handleEvents()` 操作符允许你在 Xcode 内设置断点。 74 | 这允许你立即进入调试器,检查流经管道的数据,或获取订阅者的引用,或在失败的 completion 事件中获取错误信息。 75 | ==== 76 | 77 | // force a page break - in HTML rendering is just a
78 | <<< 79 | ''' 80 | -------------------------------------------------------------------------------- /docs_zh-CN/pattern-future.adoc: -------------------------------------------------------------------------------- 1 | [#patterns-future] 2 | == 用 Future 来封装异步请求以创建一次性的发布者 3 | 4 | __目的__:: 5 | 6 | * 使用 `Future` 将异步请求转换为发布者,以便在 Combine 管道中使用返回结果。 7 | 8 | __参考__:: 9 | 10 | * <> 11 | 12 | __另请参阅__:: 13 | 14 | * <> 15 | 16 | __代码和解释__:: 17 | 18 | [source, swift] 19 | ---- 20 | import Contacts 21 | let futureAsyncPublisher = Future { promise in <1> 22 | CNContactStore().requestAccess(for: .contacts) { grantedAccess, err in <2> 23 | // err is an optional 24 | if let err = err { <3> 25 | return promise(.failure(err)) 26 | } 27 | return promise(.success(grantedAccess)) <4> 28 | } 29 | }.eraseToAnyPublisher() 30 | ---- 31 | 32 | <1> `Future` 本身由你定义返回类型,并接受一个闭包。 33 | 它给出一个与类型描述相匹配的 `Result` 对象,你可以与之交互。 34 | <2> 只要传入的闭包符合类型要求,任何异步的 API 你都可以调用。 35 | <3> 在异步 API 完成的回调中,由你决定什么是失败还是成功。 36 | 对 `promise(.failure())` 的调用返回一个失败的结果。 37 | <4> 或者调用 `promise(.success())` 返回一个值。 38 | 39 | [NOTE] 40 | ==== 41 | <> 在创建时立即发起其中异步 API 的调用,*而不是* 当它收到订阅需求时。 42 | 这可能不是你想要或需要的行为。 43 | 如果你希望在订阅者请求数据时再发起调用,你可能需要用 <> 来包装 Future。 44 | ==== 45 | 46 | 如果您想返回一个已经被解析的 promise 作为 `Future` 发布者,你可以在闭包中立即返回你想要的结果。 47 | 48 | 以下示例将单个值 `true` 返回表示成功。 49 | 你同样可以简单地返回 `false`,发布者仍然会将其作为一个成功的 promise。 50 | 51 | [source, swift] 52 | ---- 53 | let resolvedSuccessAsPublisher = Future { promise in 54 | promise(.success(true)) 55 | }.eraseToAnyPublisher() 56 | ---- 57 | 58 | 一个返回 `Future` 发布者的例子,它立即将 promise 解析为错误。 59 | 60 | [source, swift] 61 | ---- 62 | enum ExampleFailure: Error { 63 | case oneCase 64 | } 65 | 66 | let resolvedFailureAsPublisher = Future { promise in 67 | promise(.failure(ExampleFailure.oneCase)) 68 | }.eraseToAnyPublisher() 69 | ---- 70 | 71 | // force a page break - in HTML rendering is just a
72 | <<< 73 | ''' 74 | -------------------------------------------------------------------------------- /docs_zh-CN/pattern-notificationcenter.adoc: -------------------------------------------------------------------------------- 1 | [#patterns-notificationcenter] 2 | == 响应 NotificationCenter 的更新 3 | 4 | __目的__:: 5 | 6 | * 作为发布者接收 NotificationCenter 的通知,以声明式的对所提供的信息做出响应。 7 | 8 | __参考__:: 9 | 10 | * <> 11 | 12 | __另请参阅__:: 13 | 14 | * 单元测试在 https://github.com/heckj/swiftui-notes/blob/master/UsingCombineTests/NotificationCenterPublisherTests.swift[`UsingCombineTests/NotificationCenterPublisherTests.swift`] 15 | 16 | __代码和解释__:: 17 | 18 | 大量的框架和用户界面组件通过 NotificationCenter 的通知提供有关其状态和交互的信息。 19 | Apple 的文档包括一篇关于 https://developer.apple.com/documentation/combine/receiving_and_handling_events_with_combine[receiving and handling events with Combine] 的文章,特别提及了 NotificationCenter。 20 | 21 | 通过 https://developer.apple.com/documentation/foundation/notificationcenter[NotificationCenter] 发送的 https://developer.apple.com/documentation/foundation/notification[Notifications] 为你应用中的事件提供了一个通用的中心化的位置。 22 | 23 | 你还可以将自己的通知添加到你的应用程序中,在发送通知时,还可以在其 `userInfo` 属性中添加一个额外的字典来发送数据。 24 | 一个定义你自己通知的示例 `.myExampleNotification`: 25 | 26 | [source, swift] 27 | ---- 28 | extension Notification.Name { 29 | static let myExampleNotification = Notification.Name("an-example-notification") 30 | } 31 | ---- 32 | 33 | 通知名称是基于字符串的结构体。 34 | 当通知发布到 NotificationCenter 时,可以传递对象引用,表明发送通知的具体对象。 35 | 此外,通知可以包括 `userInfo`,是一个 `[AnyHashable : Any]?` 类型的值。 36 | 这允许将任意的字典(无论是引用类型还是值类型)包含在通知中。 37 | 38 | [source, swift] 39 | ---- 40 | let myUserInfo = ["foo": "bar"] 41 | 42 | let note = Notification(name: .myExampleNotification, userInfo: myUserInfo) 43 | NotificationCenter.default.post(note) 44 | ---- 45 | 46 | [NOTE] 47 | ==== 48 | 虽然在 AppKit 和 macOS 应用程序中普遍地使用了通知,但并非所有开发人员都乐于大量使用 NotificationCenter。 49 | 通知起源于更具动态性的 Objective-C runtime ,广泛利用 Any 和 optional 类型。 50 | 在 Swift 代码或管道中使用它们意味着管道必须提供类型检查并处理与预期或非预期的数据相关的任何可能错误。 51 | ==== 52 | 53 | 创建 NotificationCenter 发布者时,你提供要接收的通知的名称,并可选地提供对象引用,以过滤特定类型的对象。 54 | 属于 https://developer.apple.com/documentation/appkit/nscontrol[NSControl] 子类的多个 AppKit 组件共享了一组通知,过滤操作对于获得这些组件的正确的通知至关重要。 55 | 56 | 订阅 AppKit 生成通知的示例: 57 | 58 | [source, swift] 59 | ---- 60 | let sub = NotificationCenter.default.publisher(for: NSControl.textDidChangeNotification, <1> 61 | object: filterField) <2> 62 | .map { ($0.object as! NSTextField).stringValue } <3> 63 | .assign(to: \MyViewModel.filterString, on: myViewModel) <4> 64 | ---- 65 | <1> AppKit 中的 TextField 在值更新时生成 `textDidChangeNotification` 通知。 66 | <2> 一个 AppKit 的应用程序通常可以具有大量可能被更改的 TextField。 67 | 包含对发送控件的引用可用于过滤你特别感兴趣的文本的更改通知。 68 | <3> <> 操作符可用于获取通知中包含的对象引用,在这个例子中,发送通知的 TextField 的 `.stringValue` 属性提供了它更新后的值。 69 | <4> 由此产生的字符串可以使用可写入的 `KeyValue` 路径进行 assign。 70 | 71 | 一个订阅你自己的通知事件的示例: 72 | [source, swift] 73 | ---- 74 | let cancellable = NotificationCenter.default.publisher(for: .myExampleNotification, object: nil) 75 | // can't use the object parameter to filter on a value reference, only class references, but 76 | // filtering on 'nil' only constrains to notification name, so value objects *can* be passed 77 | // in the notification itself. 78 | .sink { receivedNotification in 79 | print("passed through: ", receivedNotification) 80 | // receivedNotification.name 81 | // receivedNotification.object - object sending the notification (sometimes nil) 82 | // receivedNotification.userInfo - often nil 83 | } 84 | ---- 85 | 86 | // force a page break - in HTML rendering is just a
87 | <<< 88 | ''' 89 | -------------------------------------------------------------------------------- /docs_zh-CN/pattern-oneshot-error-handling.adoc: -------------------------------------------------------------------------------- 1 | [#patterns-oneshot-error-handling] 2 | == 使用 catch 处理一次性管道中的错误 3 | 4 | __目的__:: 5 | 6 | * 如果你需要在管道内处理失败,例如在使用 `assign` 操作符或其他要求失败类型为 `` 的操作符之前,你可以使用 `catch` 来提供适当的逻辑。 7 | 8 | __参考__:: 9 | 10 | * <> 11 | * <> 12 | 13 | __另请参阅__:: 14 | 15 | * <> 16 | * <> 17 | * <> 18 | 19 | __代码和解释__:: 20 | 21 | <> 处理错误的方式,是将上游发布者替换为另一个发布者,这是你在闭包中用返回值提供的。 22 | 23 | [WARNING] 24 | ==== 25 | 请注意,这实际上终止了管道。 26 | 如果你使用的是一次性发布者(不创建多个事件),那这就没什么。 27 | ==== 28 | 29 | 例如,<> 是一个一次性的发布者,你可以使用 catch 在发生错误时返回默认值,以确保你得到响应结果。 30 | 扩展我们以前的示例以提供默认的响应: 31 | 32 | [source, swift] 33 | ---- 34 | struct IPInfo: Codable { 35 | // matching the data structure returned from ip.jsontest.com 36 | var ip: String 37 | } 38 | let myURL = URL(string: "http://ip.jsontest.com") 39 | // NOTE(heckj): you'll need to enable insecure downloads in your Info.plist for this example 40 | // since the URL scheme is 'http' 41 | 42 | let remoteDataPublisher = URLSession.shared.dataTaskPublisher(for: myURL!) 43 | // the dataTaskPublisher output combination is (data: Data, response: URLResponse) 44 | .map({ (inputTuple) -> Data in 45 | return inputTuple.data 46 | }) 47 | .decode(type: IPInfo.self, decoder: JSONDecoder()) <1> 48 | .catch { err in <2> 49 | return Publishers.Just(IPInfo(ip: "8.8.8.8"))<3> 50 | } 51 | .eraseToAnyPublisher() 52 | ---- 53 | 54 | <1> 通常,catch 操作符将被放置在几个可能失败的操作符之后,以便在之前任何可能的操作失败时提供回退或默认值。 55 | <2> 使用 catch 时,你可以得到错误类型,并可以检查它以选择如何提供响应。 56 | <3> Just 发布者经常用于启动另一个一次性管道,或在发生失败时直接提供默认的响应。 57 | 58 | 此技术的一个可能问题是,如果你希望原始发布者生成多个响应值,但使用 catch 之后原始管道就已结束了。 59 | 如果你正在创建一条对 <> 属性做出响应的管道,那么在任何失败值激活 catch 操作符之后,管道将不再做出进一步响应。 60 | 有关此工作原理的详细信息,请参阅 <>。 61 | 62 | 如果你要继续响应错误并处理它们,请参阅 <>。 63 | 64 | // force a page break - in HTML rendering is just a
65 | <<< 66 | ''' 67 | -------------------------------------------------------------------------------- /docs_zh-CN/pattern-retry.adoc: -------------------------------------------------------------------------------- 1 | [#patterns-retry] 2 | == 在发生暂时失败时重试 3 | 4 | __目的__:: 5 | 6 | * 当 `.failure` 发生时,<> 操作符可以被包含在管道中以重试订阅。 7 | 8 | __参考__:: 9 | 10 | * <> 11 | * <> 12 | * <> 13 | * <> 14 | 15 | __另请参阅__:: 16 | 17 | * <> 18 | * <> 19 | 20 | __代码和解释__:: 21 | 22 | 当向 `dataTaskPublisher` 请求数据时,请求可能会失败。 23 | 在这种情况下,你将收到一个带有 error 的 `.failure` 事件。 24 | 当失败时,<> 操作符将允许你对相同请求进行一定次数的重试。 25 | 当发布者不发送 `.failure` 事件时,`retry` 操作符会传递结果值。 26 | `retry` 仅在发送 `.failure` 事件时才在 Combine 管道内做出响应。 27 | 28 | 当 `retry` 收到 `.failure` 结束事件时,它重试的方式是给它所链接的操作符或发布者重新创建订阅。 29 | 30 | 当尝试请求连接不稳定的网络资源时,通常需要 <> 操作符,或者再次请求时可能会成功的情况。 31 | 如果指定的重试次数全部失败,则将 `.failure` 结束事件传递给订阅者。 32 | 33 | 在下面的示例中,我们将 retry 与 <> 操作符相结合使用。 34 | 我们使用延迟操作符在下一个请求之前使其出现少量随机延迟。 35 | 这使得重试的尝试行为被分隔开,使重试不会快速连续的发生。 36 | 37 | 此示例还包括使用 <> 操作符以更全面地检查从 `dataTaskPublisher` 返回的任何 URL 响应。 38 | 服务器的任何响应都由 `URLSession` 封装,并作为有效的响应转发。 39 | `URLSession` 不将 _404 Not Found_ 的 http 响应视为错误响应,也不将任何 _50x_ 错误代码视作错误。 40 | 使用 `tryMap`,我们可检查已发送的响应代码,并验证它是 200 的成功响应代码。 41 | 在此示例中,如果响应代码不是 200 ,则会抛出一个异常 —— 这反过来又会导致 tryMap 操作符传递 `.failure` 事件,而不是数据。 42 | 此示例将 `tryMap` 设置在 retry 操作符 *之后*,以便仅在网站未响应时重新尝试请求。 43 | 44 | [source, swift] 45 | ---- 46 | let remoteDataPublisher = urlSession.dataTaskPublisher(for: self.URL!) 47 | .delay(for: DispatchQueue.SchedulerTimeType.Stride(integerLiteral: Int.random(in: 1..<5)), scheduler: backgroundQueue) <1> 48 | .retry(3) <2> 49 | .tryMap { data, response -> Data in <3> 50 | guard let httpResponse = response as? HTTPURLResponse, 51 | httpResponse.statusCode == 200 else { 52 | throw TestFailureCondition.invalidServerResponse 53 | } 54 | return data 55 | } 56 | .decode(type: PostmanEchoTimeStampCheckResponse.self, decoder: JSONDecoder()) 57 | .subscribe(on: backgroundQueue) 58 | .eraseToAnyPublisher() 59 | ---- 60 | 61 | <1> <> 操作符将流经过管道的结果保持一小段时间,在这个例子中随机选择1至5秒。通过在管道中添加延迟,即使原始请求成功,重试也始终会发生。 62 | <2> 重试被指定为尝试3次。 63 | 如果每次尝试都失败,这将导致总共 4 次尝试 - 原始请求和 3 次额外尝试。 64 | <3> tryMap 被用于检查 dataTaskPublisher 返回的数据,如果服务器的响应数据有效,但不是 200 HTTP 响应码,则返回 `.failure` 完成事件。 65 | 66 | [WARNING] 67 | ==== 68 | 使用 <> 操作符与 <> 时,请验证你请求的 URL 如果反复请求或重试,不会产生副作用。 69 | 理想情况下,此类请求应具有幂等性。 70 | 如果没有,<> 操作符可能会发出多个请求,并产生非常意想不到的副作用。 71 | ==== 72 | 73 | // force a page break - in HTML rendering is just a
74 | <<< 75 | ''' 76 | -------------------------------------------------------------------------------- /docs_zh-CN/pattern-sink.adoc: -------------------------------------------------------------------------------- 1 | [#patterns-sink-subscriber] 2 | == 使用 sink 创建一个订阅者 3 | 4 | __目的__:: 5 | 6 | * 要接收来自发布者或管道生成的输出以及错误或者完成消息,你可以使用 <> 创建一个订阅者。 7 | 8 | __参考__:: 9 | 10 | * <> 11 | 12 | __另请参阅__:: 13 | 14 | * <> 15 | * <> 16 | * <> 17 | 18 | __代码和解释__:: 19 | 20 | Sink 创建了一个通用订阅者来捕获或响应来自 Combine 管道的数据,同时支持取消和 <>。 21 | 22 | .简单的 sink 例子 23 | [source, swift] 24 | ---- 25 | let cancellablePipeline = publishingSource.sink { someValue in <1> 26 | // do what you want with the resulting value passed down 27 | // be aware that depending on the publisher, this closure 28 | // may be invoked multiple times. 29 | print(".sink() received \(someValue)") 30 | }) 31 | ---- 32 | <1> 简单版本的 sink 是非常简洁的,跟了一个尾随闭包来接收从管道发送来的数据。 33 | 34 | .带有完成事件和数据的 sink 35 | [source, swift] 36 | ---- 37 | let cancellablePipeline = publishingSource.sink(receiveCompletion: { completion in <1> 38 | switch completion { 39 | case .finished: 40 | // no associated data, but you can react to knowing the 41 | // request has been completed 42 | break 43 | case .failure(let anError): 44 | // do what you want with the error details, presenting, 45 | // logging, or hiding as appropriate 46 | print("received the error: ", anError) 47 | break 48 | } 49 | }, receiveValue: { someValue in 50 | // do what you want with the resulting value passed down 51 | // be aware that depending on the publisher, this closure 52 | // may be invoked multiple times. 53 | print(".sink() received \(someValue)") 54 | }) 55 | 56 | cancellablePipeline.cancel() <2> 57 | ---- 58 | 59 | <1> Sinks 是通过发布者或管道中的代码链创建的,并为管道提供终点。 60 | 当 sink 在发布者创建或调用时,它通过 `subscribe` 方法隐式地开始了 <>,并请求无限制的数据。 61 | <2> Sinks 是可取消的订阅者。在任何时候,你可以使用 sink 末端对其的引用,并在上面调用 `.cancel()` 来使管道失效并关闭管道。 62 | 63 | // force a page break - in HTML rendering is just a
64 | <<< 65 | ''' 66 | -------------------------------------------------------------------------------- /docs_zh-CN/pattern-template.adoc: -------------------------------------------------------------------------------- 1 | [#patterns-someName] 2 | == Pattern N: summary description 3 | 4 | __Goal__:: 5 | 6 | * what the pattern does and why we want to use it 7 | 8 | __References__:: 9 | 10 | * << link to reference pages>> 11 | 12 | __See also__:: 13 | 14 | * << link to other patterns>> 15 | 16 | explanation w/ code 17 | 18 | // force a page break - in HTML rendering is just a
19 | <<< 20 | -------------------------------------------------------------------------------- /docs_zh-CN/pattern-test-entwine.adoc: -------------------------------------------------------------------------------- 1 | [#patterns-testable-publisher-subscriber] 2 | == 使用 EntwineTest 创建可测试的发布器和订阅者 3 | 4 | __目的__:: 5 | 6 | * 当你想要测试的是管道的时序时,用于测试管道或订阅者。 7 | 8 | __参考__:: 9 | 10 | * https://github.com/heckj/swiftui-notes/blob/master/UsingCombineTests/EntwineTestExampleTests.swift[UsingCombineTests/EntwineTestExampleTests.swift] 11 | 12 | __另请参阅__:: 13 | 14 | * <> 15 | * <> 16 | * <> 17 | * <> 18 | 19 | __代码和解释__:: 20 | 21 | EntwineTest 库可在 gitHub https://github.com/tcldr/Entwine.git 找到,为使管道可测试提供了一些额外的选择。 22 | 除了虚拟时间调度器外,EntwineTest 还有一个 `TestablePublisher` 和 `TestableSubscriber`。 23 | 这些与虚拟时间调度器协调工作,允许你指定发布者生成数据的时间,并验证订阅者收到的数据。 24 | 25 | [WARNING] 26 | ==== 27 | 截至 Xcode 11.2,SwiftPM 存在影响使用 Entwine 作为测试库的 bug。 28 | 详细信息可在 Swift 的开源 bug 报告中找到 https://bugs.swift.org/plugins/servlet/mobile#issue/SR-11564[SR-11564]。 29 | 30 | 如果使用 Xcode 11.2,你可能需要应用该解决方法,将项目设置修改为 `DEAD_CODE_STRIPPING=NO`。 31 | ==== 32 | 33 | 包含在 EntwineTest 项目中的一个这样的例子: 34 | 35 | .https://github.com/heckj/swiftui-notes/blob/master/UsingCombineTests/EntwineTestExampleTests.swift[UsingCombineTests/EntwineTestExampleTests.swift - testMap] 36 | [source, swift] 37 | ---- 38 | import XCTest 39 | import EntwineTest 40 | // library loaded from 41 | // https://github.com/tcldr/Entwine/blob/master/Assets/EntwineTest/README.md 42 | // as a Swift package https://github.com/tcldr/Entwine.git : 0.6.0, 43 | // Next Major Version 44 | 45 | class EntwineTestExampleTests: XCTestCase { 46 | 47 | func testMap() { 48 | 49 | let testScheduler = TestScheduler(initialClock: 0) 50 | 51 | // creates a publisher that will schedule its elements relatively 52 | // at the point of subscription 53 | let testablePublisher: TestablePublisher = testScheduler.createRelativeTestablePublisher([ <1> 54 | (100, .input("a")), 55 | (200, .input("b")), 56 | (300, .input("c")), 57 | ]) 58 | 59 | // a publisher that maps strings to uppercase 60 | let subjectUnderTest = testablePublisher.map { $0.uppercased() } 61 | 62 | // uses the method described above (schedules a subscription at 200 63 | // to be cancelled at 900) 64 | let results = testScheduler.start { subjectUnderTest } <2> 65 | 66 | XCTAssertEqual(results.recordedOutput, [ <3> 67 | (200, .subscription), 68 | // subscribed at 200 69 | (300, .input("A")), 70 | // received uppercased input @ 100 + subscription time 71 | (400, .input("B")), 72 | // received uppercased input @ 200 + subscription time 73 | (500, .input("C")), 74 | // received uppercased input @ 300 + subscription time 75 | ]) 76 | } 77 | } 78 | ---- 79 | 80 | <1> `TestablePublisher` 允许你设置一个在特定时间返回特定值的发布者。 81 | 在这个例子中,它会以相同的间隔返回 3 个值。 82 | <2> 当你使用虚拟时间调度器时,重要的是要确保从 start 开始调用它。 83 | 这会启动虚拟时间调度器,它的运行速度可以比时钟快,因为它只需要增加虚拟时间,而不是等待真实过去的时间。 84 | <3> `results` 是一个 TestableSubscriber 对象,包括 `recordedOutput` 属性,该属性提供所有数据的有序列表,并将控制事件的交互与其时间组合在一起。 85 | 86 | 如果这个测试序列是用 asyncAfter 完成的,那么测试将至少需要 500ms 才能完成。 87 | 当我在我的笔记本电脑上运行此测试时,它记录花费了 0.0121 秒以完成测试(12.1ms)。 88 | 89 | [NOTE] 90 | ==== 91 | EntwineTest 的副作用是,使用虚拟时间调度器的测试比实时时钟运行速度快得多。 92 | 使用实时调度机制来延迟数据发送值的相同测试可能需要更长的时间才能完成。 93 | ==== 94 | 95 | // force a page break - in HTML rendering is just a
96 | <<< 97 | ''' 98 | -------------------------------------------------------------------------------- /docs_zh-CN/pattern-test-pipeline-expectation.adoc: -------------------------------------------------------------------------------- 1 | 2 | [#patterns-testing-publisher] 3 | == 使用 XCTestExpectation 测试发布者 4 | 5 | __目的__:: 6 | 7 | * 用于测试发布者(以及连接的任何管道) 8 | 9 | __参考__:: 10 | 11 | * https://github.com/heckj/swiftui-notes/blob/master/UsingCombineTests/DataTaskPublisherTests.swift[UsingCombineTests/DataTaskPublisherTests.swift] 12 | * https://github.com/heckj/swiftui-notes/blob/master/UsingCombineTests/EmptyPublisherTests.swift[UsingCombineTests/EmptyPublisherTests.swift] 13 | * https://github.com/heckj/swiftui-notes/blob/master/UsingCombineTests/FuturePublisherTests.swift[UsingCombineTests/FuturePublisherTests.swift] 14 | * https://github.com/heckj/swiftui-notes/blob/master/UsingCombineTests/PublisherTests.swift[UsingCombineTests/PublisherTests.swift] 15 | * https://github.com/heckj/swiftui-notes/blob/master/UsingCombineTests/DebounceAndRemoveDuplicatesPublisherTests.swift[UsingCombineTests/DebounceAndRemoveDuplicatesPublisherTests.swift] 16 | 17 | __另请参阅__:: 18 | 19 | * <> 20 | * <> 21 | * <> 22 | 23 | __代码和解释__:: 24 | 25 | 当你测试发布者或创建发布者的某些代码时,你可能无法控制发布者何时返回数据以进行测试。 26 | 由其订阅者驱动的 Combine 可以设置一个同步事件来启动数据流。 27 | 你可以使用 https://developer.apple.com/documentation/xctest/xctestexpectation[XCTestExpectation] 等待一段确定的时间之后,再调用 completion 闭包进行测试。 28 | 29 | 此与 Combine 一起使用的模式: 30 | 31 | . 在测试中设置 expectation。 32 | . 确定要测试的代码。 33 | . 设置要调用的代码,以便在执行成功的情况下,你调用 expectation 的 `.fulfill()` 函数。 34 | . 设置具有明确超时时间的 `wait()` 函数,如果 expectation 在该时间窗口内未调用 `fulfill()`,则测试将失败。 35 | 36 | 如果你正在测试管道中的结果数据,那么在 <> 操作符的 `receiveValue` 闭包中触发 `fulfill()` 函数是非常方便的。 37 | 如果你正在测试管道中的失败情况,则通常在 <> 操作符的 `receiveCompletion` 闭包中包含 `fulfill()` 方法是有效的。 38 | 39 | 下列示例显示使用 expectation 测试一次性发布者(本例中是 <>),并期望数据在不出错的情况下流动。 40 | 41 | .https://github.com/heckj/swiftui-notes/blob/master/UsingCombineTests/DataTaskPublisherTests.swift#L47[UsingCombineTests/DataTaskPublisherTests.swift - testDataTaskPublisher] 42 | [source, swift] 43 | ---- 44 | func testDataTaskPublisher() { 45 | // setup 46 | let expectation = XCTestExpectation(description: "Download from \(String(describing: testURL))") <1> 47 | let remoteDataPublisher = URLSession.shared.dataTaskPublisher(for: self.testURL!) 48 | // validate 49 | .sink(receiveCompletion: { fini in 50 | print(".sink() received the completion", String(describing: fini)) 51 | switch fini { 52 | case .finished: expectation.fulfill() <2> 53 | case .failure: XCTFail() <3> 54 | } 55 | }, receiveValue: { (data, response) in 56 | guard let httpResponse = response as? HTTPURLResponse else { 57 | XCTFail("Unable to parse response an HTTPURLResponse") 58 | return 59 | } 60 | XCTAssertNotNil(data) 61 | // print(".sink() data received \(data)") 62 | XCTAssertNotNil(httpResponse) 63 | XCTAssertEqual(httpResponse.statusCode, 200) <4> 64 | // print(".sink() httpResponse received \(httpResponse)") 65 | }) 66 | 67 | XCTAssertNotNil(remoteDataPublisher) 68 | wait(for: [expectation], timeout: 5.0) <5> 69 | } 70 | ---- 71 | 72 | <1> Expectation 设置为一个字符串,这样在发生失败时更容易调试。 73 | 此字符串仅在测试失败时才能看到。 74 | 我们在这里测试的代码是 `dataTaskPublisher` 从测试前就已定义好的预设的 URL 中取回数据。 75 | 发布者通过将 <> 订阅者连接到它开始触发请求。 76 | 如果没有 expectation,代码仍将运行,但构建的测试运行结构将不会等到结果返回之后再去检查是否有任何意外。 77 | 测试中的 expectation "暂停测试" 去等待响应,让操作符先发挥它们的作用。 78 | <2> 在这个例子中,测试期望可以成功完成并正常终止,因此在 `receiveCompletion` 闭包内调用 `expectation.fulfill()`,具体是接收到 `.finished` completion 后调用。 79 | <3> 由于我们不期望失败,如果我们收到 `.failure` completion,我们也明确地调用 `XCTFail()`。 80 | <4> 我们在 `receiveValue` 中还有一些其他断言。 81 | 由于此发布者设置返回单个值然后终止,因此我们可以对收到的数据进行内联断言。 82 | 如果我们收到多个值,那么我们可以收集这些值,并就事后收到的内容做出断言。 83 | <5> 此测试使用单个 expectation,但你可以包含多个独立的 expectation,去要求它们都被 `fulfill()`。 84 | 它还规定此测试的最长运行时间为 5 秒。 85 | 测试并不总是需要五秒钟,因为一旦收到 fulfill,它就会完成。 86 | 如果出于某种原因,测试需要超过五秒钟的响应时间,XCTest 将报告测试失败。 87 | 88 | // force a page break - in HTML rendering is just a
89 | <<< 90 | ''' 91 | -------------------------------------------------------------------------------- /docs_zh-CN/patterns.adoc: -------------------------------------------------------------------------------- 1 | [#patterns] 2 | = 常用模式和方法 3 | 4 | 本章包括一系列模式和发布者、订阅者和管道的示例。 5 | 这些示例旨在说明如何使用 Combine 框架完成各种任务。 6 | 7 | include::pattern-sink.adoc[] 8 | include::pattern-assign.adoc[] 9 | include::pattern-datataskpublisher-decode.adoc[] 10 | include::pattern-datataskpublisher-trymap.adoc[] 11 | include::pattern-future.adoc[] 12 | include::pattern-sequencing-operations.adoc[] 13 | 14 | [#patterns-general-error-handling] 15 | == 错误处理 16 | 17 | 上述示例都假设,如果发生错误情况,订阅者将处理这些情况。 18 | 但是,你并不总是能够控制订阅者的要求——如果你使用 SwiftUI,情况可能如此。 19 | 在这些情况下,你需要构建管道,以便输出类型与订阅者的类型匹配。 20 | 这意味着你在处理管道内的任何错误。 21 | 22 | 例如,如果你正在使用 SwiftUI,并且你希望使用 <> 在按钮上设置 `isEnabled` 属性,则订阅者将有几个要求: 23 | 24 | . 订阅者应匹配 `` 的类型输出 25 | . 应该在主线程调用订阅者 26 | 27 | 如果发布者抛出一个错误(例如 <> ),你需要构建一个管道来转换输出类型,还需要处理管道内的错误,以匹配错误类型 ``。 28 | 29 | 如何处理管道内的错误取决于管道的定义方式。 30 | 如果管道设置为返回单个结果并终止, 一个很好的例子就是 <>。 31 | 如果管道被设置为持续更新,则错误处理要复杂一点。 32 | 这种情况下的一个很好的例子是 <>。 33 | 34 | :leveloffset: +1 35 | include::pattern-assertnofailure.adoc[] 36 | include::pattern-oneshot-error-handling.adoc[] 37 | include::pattern-retry.adoc[] 38 | include::pattern-continual-error-handling.adoc[] 39 | include::pattern-constrained-network.adoc[] 40 | :leveloffset: -1 41 | 42 | [#patterns-uikit-integration] 43 | == 和 UIKit 或 AppKit 集成 44 | 45 | :leveloffset: +1 46 | include::pattern-update-interface-userinput.adoc[] 47 | include::pattern-cascading-update-interface.adoc[] 48 | include::pattern-merging-streams-interface.adoc[] 49 | include::pattern-delegate-publisher-subject.adoc[] 50 | include::pattern-notificationcenter.adoc[] 51 | :leveloffset: -1 52 | 53 | [#patterns-swiftui-integration] 54 | == 和 SwiftUI 集成 55 | 56 | :leveloffset: +1 57 | include::pattern-observableobject.adoc[] 58 | :leveloffset: -1 59 | 60 | [#patterns-testing-and-debugging] 61 | == 测试和调试 62 | 63 | Combine 中的发布者和订阅者接口是非常易于测试的。 64 | 65 | 借助 Combine 的可组合性,你可以利用此优势创建或消费符合 https://developer.apple.com/documentation/combine/publisher[Publisher] 协议的 API。 66 | 67 | 以 https://developer.apple.com/documentation/combine/publisher[publisher protocol] 为关键接口,你可以替换任何一方以单独验证你的代码。 68 | 69 | 例如,如果你的代码专注于通过 Combine 从外部 Web 服务中提供其数据,则可能会使此接口遵循 `AnyPublisher`。 70 | 然后,你可以使用该接口独立测试管道的任何一侧。 71 | 72 | * 你可以模拟 API 请求和可能响应的数据,包括各种错误条件。 73 | 这可以包括使用 <> 或 <> 创建的发布者来返回数据,或者更复杂的使用 <>。 74 | 使用这些方案都不需要你进行实际的网络接口调用。 75 | 76 | * 同样,你也可以隔离测试,让发布者进行 API 调用,并验证预期的各种成功和失败条件。 77 | 78 | :leveloffset: +1 79 | include::pattern-test-pipeline-expectation.adoc[] 80 | include::pattern-test-subscriber-subject.adoc[] 81 | include::pattern-test-subscriber-scheduled.adoc[] 82 | include::pattern-test-entwine.adoc[] 83 | include::pattern-debugging-pipelines-print.adoc[] 84 | include::pattern-debugging-pipelines-handleevents.adoc[] 85 | include::pattern-debugging-pipelines-breakpoint.adoc[] 86 | :leveloffset: -1 87 | 88 | // force a page break - ignored in HTML rendering 89 | <<< 90 | ''' 91 | -------------------------------------------------------------------------------- /docs_zh-CN/raw-notes.adoc: -------------------------------------------------------------------------------- 1 | [#raw-notes] 2 | = Raw Notes 3 | 4 | Words and phrases to look for and destroy: 5 | 6 | "simply" 7 | "just" 8 | "and then" 9 | "are then" 10 | "you should" 11 | 12 | Look for an verify: 13 | 14 | "stream" vs pipeline 15 | 16 | Things to verify are consistent: 17 | 18 | `finished` -> `.finished` 19 | `failure` -> `.failure` 20 | 21 | `.failure` completion 22 | `.finished` completion -------------------------------------------------------------------------------- /docs_zh-CN/reference-template.adoc: -------------------------------------------------------------------------------- 1 | __Summary__:: 2 | 3 | n/a 4 | 5 | __Constraints on connected publisher__:: 6 | 7 | * __none__ 8 | 9 | __icon:apple[set=fab] docs__:: n/a 10 | 11 | __Usage__:: 12 | 13 | n/a 14 | 15 | __Details__:: 16 | 17 | n/a -------------------------------------------------------------------------------- /docs_zh-CN/using-combine_zh-CN.adoc: -------------------------------------------------------------------------------- 1 | = Using Combine 2 | Joseph Heck 3 | v 1.2.2, 2021-05-24 4 | :doctype: book 5 | :creator: {author} 6 | :producer: Joseph Heck 7 | :keywords: Apple, Combine, ReactiveX, SwiftUI 8 | :copyright: Joseph Heck 2019-2022 9 | :publication-type: book 10 | // NOTE use 'anthology' for per-chapter author support 11 | :idprefix: 12 | :idseparator: - 13 | :imagesdir: images 14 | :front-cover-image: image:UsingCombineWithSwiftCoverArt_2560x1920.png[fit=cover] 15 | :google-analytics-account: UA-898243-5 16 | :source-highlighter: rouge 17 | :url-issues: https://github.com/heckj/swiftui-notes/issues 18 | :toc: left 19 | :toclevels: 4 20 | // enable font-awesome icons in content 21 | :icons: font 22 | 23 | ifndef::ebook-format[:leveloffset: 1] 24 | 25 | include::aboutthisbook.adoc[] 26 | include::introduction.adoc[] 27 | include::coreconcepts.adoc[] 28 | include::developingwith.adoc[] 29 | include::patterns.adoc[] 30 | include::reference.adoc[] 31 | -------------------------------------------------------------------------------- /marbles/README.md: -------------------------------------------------------------------------------- 1 | # Diagramming 2 | 3 | RxMarbles diagramming tool 4 | 5 | documented: 6 | source: 7 | diagram syntax: 8 | 9 | ## install 10 | 11 | virtualenv .venv 12 | source .venv/bin/activate 13 | pip3 install -r requirements.txt 14 | 15 | ## generate and move to use them in the content 16 | 17 | marblesgen -v diagrams/* 18 | mv *.svg ../docs/images/diagrams/ 19 | -------------------------------------------------------------------------------- /marbles/diagrams/newtest.txt: -------------------------------------------------------------------------------- 1 | marble endings 2 | { 3 | source foo: +---> // this one with no specific end 4 | source bar: +---| // this one ending with completion 5 | source dog: +---# // this one ending with error 6 | } -------------------------------------------------------------------------------- /marbles/diagrams/test.txt: -------------------------------------------------------------------------------- 1 | marble test_example 2 | { 3 | source a: +--A-B--C-# 4 | operator example: +--1-2--3-| 5 | } 6 | -------------------------------------------------------------------------------- /marbles/requirements.txt: -------------------------------------------------------------------------------- 1 | rxmarbles 2 | -------------------------------------------------------------------------------- /re.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Joe's Handy Rebuild-it-all-and-test-things script 4 | # 5 | # to rebuild the HTML locally (using Docker) and open it for inspection: 6 | # export OPENIT=1 7 | # ./re.bash 8 | 9 | # if you want to just rebuild, but not open, leave off the "OPENIT" setting 10 | # if you want to rebuild and open everything, you can set all the 11 | # environment variables like so: 12 | # 13 | # export REBUILDEPUB=1 14 | # export REBUILDPDF=1 15 | # export OPENIT=1 16 | # ./re.bash 17 | 18 | # NOTE(heckj) 20nov2019 - no longer generating the marble diagrams 19 | #echo "generating diagrams" 20 | #docker run --rm -i -v $(pwd)/marbles/diagrams:/data --name rxmarbles heckj/rxmarbles 21 | #mv $(pwd)/marbles/diagrams/*.svg $(pwd)/docs/images/diagrams/ 22 | 23 | echo "Rendering HTML" 24 | # render the HTML, results will appear in `output` directory 25 | docker run --platform linux/amd64 --rm -v $(pwd):/documents/ --name asciidoc-to-html asciidoctor/docker-asciidoctor asciidoctor -v -t -D /documents/output -r ./docs/lib/google-analytics-docinfoprocessor.rb docs/using-combine-book.adoc 26 | # output appears into ./output/using-combine-book.html 27 | docker run --platform linux/amd64 --rm -v $(pwd):/documents/ --name asciidoc-to-html asciidoctor/docker-asciidoctor asciidoctor -v -t -D /documents/output -r ./docs/lib/google-analytics-docinfoprocessor.rb docs_zh-CN/using-combine_zh-CN.adoc 28 | 29 | # copy in the images for the HTML 30 | mkdir -p output/images 31 | cp -r docs/images/* output/images 32 | 33 | if [ -n "${OPENIT}" ]; then 34 | open output/using-combine-book.html 35 | fi 36 | 37 | # if ENV VAR 'REBUILDPDF' is set, then invoke 38 | if [ -n "${REBUILDPDF}" ]; then 39 | # render a PDF, results will appear in `output` directory 40 | echo "Rendering PDF" 41 | docker run --platform linux/amd64 --rm -v $(pwd):/documents/ --name asciidoc-to-pdf asciidoctor/docker-asciidoctor asciidoctor-pdf -v -t -D /documents/output docs/using-combine-book.adoc 42 | if [ -n "${OPENIT}" ]; then 43 | open output/using-combine-book.pdf 44 | fi 45 | fi 46 | 47 | # if ENV VAR 'REBUILDEPUB' is set, then invoke 48 | if [ -n "${REBUILDEPUB}" ]; then 49 | # render an epub3 file, will should appear in `output` directory 50 | echo "Rendering ePub" 51 | docker run --platform linux/amd64 --rm -v $(pwd):/documents/ --name asciidoc-to-epub3 asciidoctor/docker-asciidoctor asciidoctor-epub3 -v -t -D /documents/output docs/using-combine-book.adoc 52 | if [ -n "${OPENIT}" ]; then 53 | open output/using-combine-book.epub 54 | fi 55 | fi 56 | -------------------------------------------------------------------------------- /testpages/swift.adoc: -------------------------------------------------------------------------------- 1 | = testing rendering of swift source sample 2 | :source-highlighter: pygments 3 | 4 | .swiftcode 5 | [source,swift] 6 | ---- 7 | enum sampleError: Error { 8 | case exampleError 9 | } 10 | 11 | let x = Publishers.Future { promise in 12 | do { 13 | try self.myFunctionCall(someVariable) { varname in 14 | promise(.success(varname ? username : nil)) 15 | } 16 | } catch { 17 | promise(.failure(sampleError.exampleError)) 18 | } 19 | } 20 | ---- 21 | 22 | .app.rb 23 | [source,ruby] 24 | ---- 25 | require 'sinatra' 26 | 27 | get '/hi' do 28 | "Hello World!" 29 | end 30 | ---- --------------------------------------------------------------------------------