├── .github
└── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
├── .gitignore
├── .travis.yml
├── LICENSE
├── Package.swift
├── QLoop.xcodeproj
├── project.pbxproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
└── xcshareddata
│ └── xcschemes
│ ├── QLoop-iOS.xcscheme
│ ├── QLoop-macOS.xcscheme
│ ├── QLoop-tvOS.xcscheme
│ └── QLoop-watchOS.xcscheme
├── README.md
├── Sources
└── QLoop
│ ├── Common
│ └── QLCommon.swift
│ ├── Iterating
│ ├── QLoopIterating.swift
│ ├── QLoopIteratorContinueNil.swift
│ ├── QLoopIteratorContinueNilMax.swift
│ ├── QLoopIteratorContinueOutput.swift
│ ├── QLoopIteratorContinueOutputMax.swift
│ └── QLoopIteratorSingle.swift
│ ├── QLAnchor+ConvenienceInit.swift
│ ├── QLAnchor.swift
│ ├── QLPath.swift
│ ├── QLoop+ConvenienceInit.swift
│ ├── QLoop.swift
│ ├── Result+ErrorGettable.swift
│ └── Segment
│ ├── AnySegment.swift
│ ├── Parallel
│ ├── QLParallelSegment+OperationRunner.swift
│ └── QLParallelSegment.swift
│ ├── QLSegment.swift
│ └── Serial
│ └── QLSerialSegment.swift
├── Tests
├── Info.plist
├── LinuxMain.swift
└── QLoopTests
│ ├── Common
│ └── QLCommonConfigAnchorTests.swift
│ ├── FibonacciTest.swift
│ ├── Iterating
│ ├── QLoopIteratorContinueNilMaxTests copy.swift
│ ├── QLoopIteratorContinueNilTests copy.swift
│ ├── QLoopIteratorContinueOutputMaxTests.swift
│ ├── QLoopIteratorContinueOutputTests.swift
│ └── QLoopIteratorSingleTests.swift
│ ├── QLAnchorTests.swift
│ ├── QLPathTests.swift
│ ├── QLoop+ConvenienceInitTests.swift
│ ├── QLoopTests.swift
│ ├── Segment
│ ├── QLParallelSegmentTests.swift
│ ├── QLSegmentTests.swift
│ └── QLSerialSegmentTests.swift
│ ├── XCTestManifests.swift
│ ├── helpers
│ └── CapturedCompletion.swift
│ └── mocks
│ ├── MockComponent.swift
│ ├── MockLoopIterable.swift
│ ├── MockLoopIterator.swift
│ ├── MockOperation.swift
│ └── SpyAnchor.swift
└── docs
├── changelog.md
├── getting-started.md
├── icon.png
├── introduction.md
├── loops.png
├── reference.md
├── reference
├── QLAnchor.md
├── QLParallelSegment.md
├── QLPath.md
├── QLSerialSegment.md
└── QLoop.md
└── segments.png
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 |
16 | **Expected behavior**
17 | A clear and concise description of what you expected to happen.
18 |
19 | **Additional context**
20 | Add any other context about the problem here.
21 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /Packages
3 | .build/
4 | build/
5 | DerivedData/
6 | *.pbxuser
7 | !default.pbxuser
8 | *.mode1v3
9 | !default.mode1v3
10 | *.mode2v3
11 | !default.mode2v3
12 | *.perspectivev3
13 | !default.perspectivev3
14 | xcuserdata/
15 | *.moved-aside
16 | *.xccheckout
17 | *.xcscmblueprint
18 | *.hmap
19 | *.ipa
20 | *.dSYM.zip
21 | *.dSYM
22 | timeline.xctimeline
23 | playground.xcworkspace
24 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: swift
2 | osx_image: xcode10.2
3 | xcode_project: QLoop.xcodeproj
4 | xcode_scheme: QLoop-iOS
5 | xcode_destination: platform=iOS Simulator,name=iPhone X
6 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License
2 |
3 | Copyright (c) 2018-2019 James Hall
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 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:4.2
2 |
3 | import PackageDescription
4 |
5 | let package = Package(
6 | name: "QLoop",
7 | products: [
8 | .library(name: "QLoop", targets: ["QLoop"]),
9 | ],
10 | dependencies: [],
11 | targets: [
12 | .target(name: "QLoop", dependencies: []),
13 | .testTarget(name: "QLoopTests", dependencies: ["QLoop"]),
14 | ]
15 | )
16 |
--------------------------------------------------------------------------------
/QLoop.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/QLoop.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/QLoop.xcodeproj/xcshareddata/xcschemes/QLoop-iOS.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
32 |
33 |
39 |
40 |
41 |
42 |
48 |
49 |
55 |
56 |
57 |
58 |
60 |
66 |
67 |
68 |
69 |
70 |
80 |
81 |
87 |
88 |
89 |
90 |
96 |
97 |
103 |
104 |
105 |
106 |
108 |
109 |
112 |
113 |
114 |
--------------------------------------------------------------------------------
/QLoop.xcodeproj/xcshareddata/xcschemes/QLoop-macOS.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
32 |
33 |
39 |
40 |
41 |
42 |
48 |
49 |
55 |
56 |
57 |
58 |
60 |
66 |
67 |
68 |
69 |
70 |
80 |
81 |
87 |
88 |
89 |
90 |
96 |
97 |
103 |
104 |
105 |
106 |
108 |
109 |
112 |
113 |
114 |
--------------------------------------------------------------------------------
/QLoop.xcodeproj/xcshareddata/xcschemes/QLoop-tvOS.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
32 |
33 |
39 |
40 |
41 |
42 |
48 |
49 |
55 |
56 |
57 |
58 |
60 |
66 |
67 |
68 |
69 |
70 |
80 |
81 |
87 |
88 |
89 |
90 |
96 |
97 |
103 |
104 |
105 |
106 |
108 |
109 |
112 |
113 |
114 |
--------------------------------------------------------------------------------
/QLoop.xcodeproj/xcshareddata/xcschemes/QLoop-watchOS.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
37 |
38 |
39 |
40 |
41 |
42 |
52 |
53 |
59 |
60 |
61 |
62 |
68 |
69 |
75 |
76 |
77 |
78 |
80 |
81 |
84 |
85 |
86 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | #  QLoop
2 |
3 | 
4 | [](https://travis-ci.com/quickthyme/qloop)
5 | [ ](https://swift.org/package-manager/)
6 | 
7 |
8 | **QLoop** /'kyoo•loop/ - *n* - Declarative asynchronous operation loops
9 |
10 | - compose asynchronous operation paths as reusable "loop" constructs
11 | - *test-friendly* observer-pattern module favoring declarative composition
12 | - built-in error propagation
13 | - swiftPM compatible package
14 | - universal module; Swift 4.2+, 5 (default)
15 |
16 | Compose `paths` of asynchronous operation `segments`, then bind them to anchors
17 | or wrap them up into *observable* loops. Simply decorate an entity with empty `loops`
18 | and/or `anchors`, and implement the `onChange` and/or `onError` events.
19 |
20 | Designed to be simple to use, test, and debug. *(Or so it's intended.)*
21 |
22 |
23 |
24 | ## [Introduction](docs/introduction.md)
25 |
26 | a.k.a. *[what it is and what it does](docs/introduction.md)*.
27 |
28 |
29 | ## [Getting Started](docs/getting-started.md)
30 |
31 | How to *[install and start using](docs/getting-started.md)* it.
32 |
33 |
34 | ## [API Reference](docs/reference.md)
35 |
36 | Basically just a listing of the *[classes, functions, and arguments](docs/reference.md)* that make up QLoop.
37 |
38 |
39 | ## [Change Log](docs/changelog.md)
40 |
41 | On-going *[summary of pertinent changes](docs/changelog.md)* from one version to the next.
42 |
43 |
44 | ## [Demo App](https://github.com/quickthyme/qloop-demo)
45 |
46 | The example app, *[qloop-demo](https://github.com/quickthyme/qloop-demo)*,
47 | demonstrates how to write a declarative iOS app using QLoop, which includes
48 | real-world working examples of static composition, error handling, concurrent
49 | threads, and unit-testing.
50 |
51 |
52 |
53 |
54 | ---
55 |
56 | Enjoying QLoop? You might check out its soul-mate:
57 | *[QRoute](https://github.com/quickthyme/qroute)*,
58 | a library providing declarative navigation and routing features with similar
59 | enthusiasm. Using them together, or separately, is up to you.
60 |
61 | :)
62 |
--------------------------------------------------------------------------------
/Sources/QLoop/Common/QLCommon.swift:
--------------------------------------------------------------------------------
1 | public struct QLCommon {
2 |
3 | public struct Config {
4 |
5 | public struct Anchor {
6 |
7 | public static var releaseValues: Bool = {
8 | #if !DEBUG
9 | return true
10 | #else
11 | return false
12 | #endif
13 | }()
14 |
15 | public static var autoThrowResultFailures: Bool = true
16 | }
17 | }
18 |
19 | public enum Error: Swift.Error {
20 | case AnchorMismatch
21 | case Unknown
22 | case ThrownButNotSet
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Sources/QLoop/Iterating/QLoopIterating.swift:
--------------------------------------------------------------------------------
1 | public protocol QLoopIterating: AnyObject {
2 | @discardableResult
3 | func iterate(_ loop: QLoopIterable) -> Bool
4 | }
5 |
6 | public protocol QLoopIteratingResettable: QLoopIterating {
7 | func reset()
8 | }
9 |
10 | public protocol QLoopIterable {
11 | var discontinue: Bool { get }
12 | func iteration()
13 | func iterationFromLastOutput()
14 | }
15 |
--------------------------------------------------------------------------------
/Sources/QLoop/Iterating/QLoopIteratorContinueNil.swift:
--------------------------------------------------------------------------------
1 | public final class QLoopIteratorContinueNil: QLoopIterating {
2 |
3 | public init() {
4 | }
5 |
6 | @discardableResult
7 | public func iterate(_ loop: QLoopIterable) -> Bool {
8 | guard (loop.discontinue == false) else { return false }
9 | loop.iteration()
10 | return true
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Sources/QLoop/Iterating/QLoopIteratorContinueNilMax.swift:
--------------------------------------------------------------------------------
1 | public final class QLoopIteratorContinueNilMax: QLoopIteratingResettable {
2 |
3 | public var iterations: Int = 0
4 | public var maxIterations: Int
5 |
6 | public init(_ maxIterations: Int) {
7 | self.maxIterations = maxIterations
8 | }
9 |
10 | public func reset() {
11 | iterations = 0
12 | }
13 |
14 | @discardableResult
15 | public func iterate(_ loop: QLoopIterable) -> Bool {
16 | guard (loop.discontinue == false) else { return false }
17 | iterations += 1
18 | if (iterations < maxIterations) {
19 | loop.iteration()
20 | return true
21 | }
22 | return false
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Sources/QLoop/Iterating/QLoopIteratorContinueOutput.swift:
--------------------------------------------------------------------------------
1 | public final class QLoopIteratorContinueOutput: QLoopIterating {
2 |
3 | public init() {
4 | }
5 |
6 | @discardableResult
7 | public func iterate(_ loop: QLoopIterable) -> Bool {
8 | guard (loop.discontinue == false) else { return false }
9 | loop.iterationFromLastOutput()
10 | return true
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Sources/QLoop/Iterating/QLoopIteratorContinueOutputMax.swift:
--------------------------------------------------------------------------------
1 | public final class QLoopIteratorContinueOutputMax: QLoopIteratingResettable {
2 |
3 | public var iterations: Int = 0
4 | public var maxIterations: Int
5 |
6 | public init(_ maxIterations: Int) {
7 | self.maxIterations = maxIterations
8 | }
9 |
10 | public func reset() {
11 | iterations = 0
12 | }
13 |
14 | @discardableResult
15 | public func iterate(_ loop: QLoopIterable) -> Bool {
16 | guard (loop.discontinue == false) else { return false }
17 | iterations += 1
18 | if (iterations < maxIterations) {
19 | loop.iterationFromLastOutput()
20 | return true
21 | }
22 | return false
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Sources/QLoop/Iterating/QLoopIteratorSingle.swift:
--------------------------------------------------------------------------------
1 | public final class QLoopIteratorSingle: QLoopIterating {
2 |
3 | public init() {
4 | }
5 |
6 | @discardableResult
7 | public func iterate(_ loop: QLoopIterable) -> Bool {
8 | return false
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/Sources/QLoop/QLAnchor+ConvenienceInit.swift:
--------------------------------------------------------------------------------
1 | public extension QLAnchor {
2 | private static func emptyIn(_ o: Input?)->() {/**/}
3 | private static func emptyErr(_ e: Error)->() {/**/}
4 |
5 | convenience init() {
6 | self.init(onChange: QLAnchor.emptyIn,
7 | onError: QLAnchor.emptyErr)
8 | }
9 |
10 | convenience init(onChange: @escaping OnChange) {
11 | self.init(onChange: onChange,
12 | onError: QLAnchor.emptyErr)
13 | }
14 |
15 | convenience init(earlyRepeaters: QLAnchor...) {
16 | self.init(echoFilter: QLAnchor.DefaultEchoFilter,
17 | earlyRepeaters: earlyRepeaters,
18 | lateRepeaters: [])
19 | }
20 |
21 | convenience init(lateRepeaters: QLAnchor...) {
22 | self.init(echoFilter: QLAnchor.DefaultEchoFilter,
23 | earlyRepeaters: [],
24 | lateRepeaters: lateRepeaters)
25 | }
26 |
27 | convenience init(earlyRepeaters: [QLAnchor],
28 | lateRepeaters: [QLAnchor]) {
29 | self.init(echoFilter: QLAnchor.DefaultEchoFilter,
30 | earlyRepeaters: earlyRepeaters,
31 | lateRepeaters: lateRepeaters)
32 | }
33 |
34 | convenience init(echoFilter: @escaping EchoFilter,
35 | earlyRepeaters: QLAnchor...) {
36 | self.init(echoFilter: echoFilter,
37 | earlyRepeaters: earlyRepeaters,
38 | lateRepeaters: [])
39 | }
40 |
41 | convenience init(echoFilter: @escaping EchoFilter,
42 | lateRepeaters: QLAnchor...) {
43 | self.init(echoFilter: echoFilter,
44 | earlyRepeaters: [],
45 | lateRepeaters: lateRepeaters)
46 | }
47 |
48 | convenience init(echoFilter: @escaping EchoFilter,
49 | earlyRepeaters: [QLAnchor],
50 | lateRepeaters: [QLAnchor]) {
51 | self.init(onChange: QLAnchor.emptyIn,
52 | onError: QLAnchor.emptyErr)
53 | self.addRepeaters(earlyRepeaters, timing: .early)
54 | self.addRepeaters(lateRepeaters, timing: .late)
55 | self.echoFilter = echoFilter
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/Sources/QLoop/QLAnchor.swift:
--------------------------------------------------------------------------------
1 | import Dispatch
2 |
3 | public protocol AnyAnchor: AnyObject {
4 | var inputSegment: AnySegment? { get }
5 | }
6 |
7 | public final class QLAnchor: AnyAnchor {
8 | public typealias OnChange = (Input?)->()
9 | public typealias OnError = (Error)->()
10 |
11 | public typealias EchoFilter = (Input?, QLAnchor) -> (Bool)
12 | internal static var DefaultEchoFilter: EchoFilter { return { _, _ in return true } }
13 |
14 | public enum Timing {
15 | case early, late
16 | }
17 |
18 | internal final class Repeater {
19 |
20 | weak var anchor: QLAnchor?
21 |
22 | let timing: Timing
23 |
24 | init(_ anchor: QLAnchor, timing: Timing) {
25 | self.timing = timing
26 | self.anchor = anchor
27 | }
28 |
29 | func echo(value: Input?, filter: EchoFilter, timing: Timing) {
30 | if let repeater = self.anchor,
31 | filter(value, repeater) {
32 | repeater.value = value
33 | }
34 | }
35 |
36 | func echo(error: Error) {
37 | anchor?.error = error
38 | }
39 | }
40 |
41 | lazy var inputQueue = DispatchQueue(label: "\(self).inputQueue",
42 | qos: .userInitiated)
43 |
44 | public required init(onChange: @escaping OnChange,
45 | onError: @escaping OnError) {
46 | self.onChange = onChange
47 | self.onError = onError
48 | }
49 |
50 | public var onChange: OnChange
51 |
52 | public var onError: OnError
53 |
54 | public var inputSegment: AnySegment?
55 |
56 | public func addRepeaters(_ repeaters: [QLAnchor], timing: Timing) {
57 | let repeaters = repeaters.map { Repeater($0, timing: timing) }
58 | self._repeaters.append(contentsOf: repeaters)
59 | }
60 |
61 | public func getRepeaters(timing: Timing) -> [QLAnchor] {
62 | return _repeaters.compactMap { ($0.timing == timing) ? $0.anchor : nil }
63 | }
64 |
65 | internal var _repeaters: [Repeater] = []
66 |
67 | public var echoFilter: EchoFilter = DefaultEchoFilter
68 |
69 | private var _value: Input?
70 | public var value: Input? {
71 | get {
72 | var safeInput: Input? = nil
73 | inputQueue.sync { safeInput = self._value }
74 | return safeInput
75 | }
76 | set {
77 | inputQueue.sync { self._value = newValue }
78 |
79 | if let err = getReroutableError(newValue) {
80 | self.error = err
81 | } else {
82 | echo(value: newValue, timing: .early)
83 | dispatch(value: newValue)
84 | echo(value: newValue, timing: .late)
85 | }
86 |
87 | if (QLCommon.Config.Anchor.releaseValues) {
88 | inputQueue.sync { self._value = nil }
89 | }
90 | }
91 | }
92 |
93 | private var _error: Error?
94 | public var error: Error? {
95 | get {
96 | var safeError: Error? = nil
97 | inputQueue.sync { safeError = self._error }
98 | return safeError
99 | }
100 | set {
101 | let err: Error = newValue ?? QLCommon.Error.ThrownButNotSet
102 | inputQueue.sync { self._error = err }
103 | dispatch(error: err)
104 | echo(error: err)
105 |
106 | if (QLCommon.Config.Anchor.releaseValues) {
107 | inputQueue.sync { self._error = nil }
108 | }
109 | }
110 | }
111 |
112 | private func getReroutableError(_ newValue: Input?) -> Error? {
113 | guard QLCommon.Config.Anchor.autoThrowResultFailures,
114 | let errGettable = newValue as? ErrorGettable,
115 | let err = errGettable.getError()
116 | else { return nil }
117 | return err
118 | }
119 |
120 | private func dispatch(value: Input?) {
121 | DispatchQueue.main.async {
122 | self.onChange(value)
123 | }
124 | }
125 |
126 | private func dispatch(error: Error) {
127 | DispatchQueue.main.async {
128 | self.onError(error)
129 | }
130 | }
131 |
132 | private func echo(value: Input?, timing: Timing) {
133 | for repeater in _repeaters {
134 | guard repeater.timing == timing else { continue }
135 | repeater.echo(value: value, filter: echoFilter, timing: timing)
136 | }
137 | }
138 |
139 | private func echo(error: Error) {
140 | for repeater in _repeaters {
141 | repeater.echo(error: error)
142 | }
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/Sources/QLoop/QLPath.swift:
--------------------------------------------------------------------------------
1 | open class QLPath {
2 | public typealias Operation = QLSegment.Operation
3 | public typealias Completion = QLSegment.Completion
4 |
5 | public required init?(_ segments: AnySegment...) {
6 |
7 | guard
8 | let lastSegment = segments.last,
9 | let firstSegment = segments.first
10 | else { return nil }
11 |
12 | do {
13 | let _: AnySegment =
14 |
15 | try segments.reduce(
16 | firstSegment,
17 | ({ result, next in
18 | guard (result !== next) else { return result }
19 | guard let _ = result.linked(to: next)
20 | else { throw QLCommon.Error.AnchorMismatch }
21 | return next
22 | })
23 | )
24 |
25 | lastSegment.applyOutputAnchor(self.output)
26 | self.input = firstSegment.inputAnchor as! QLAnchor
27 |
28 | } catch { return nil }
29 | }
30 |
31 | public final var input: QLAnchor = QLAnchor()
32 | public final var output: QLAnchor