├── .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 |
58 |
--------------------------------------------------------------------------------
/docs/images/diagrams/endings.svg:
--------------------------------------------------------------------------------
1 |
2 |
104 |
--------------------------------------------------------------------------------
/docs/images/diagrams/marble_diagram.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
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 | 这种情况下的一个很好的例子是 <