├── .gitignore
├── .swiftpm
└── xcode
│ └── package.xcworkspace
│ └── xcshareddata
│ └── IDEWorkspaceChecks.plist
├── Documentation
└── CustomStylingGuide.md
├── Package.swift
├── README.md
├── Resources
├── Idle.png
├── Logo.png
├── Styles
│ ├── Blank.png
│ ├── BlankCentered.png
│ ├── Centered.png
│ └── Primary.png
├── example_percentage.png
├── lowerbound.png
└── upperbound.png
├── Sources
└── SlidingRuler
│ ├── Enums
│ ├── SlidingRulerState.swift
│ ├── StickyMark.swift
│ └── Tick.swift
│ ├── Extensions
│ ├── BinaryFloatingPointExtension.swift
│ ├── BinaryIntegerExtension.swift
│ ├── ClosedRangeExtension.swift
│ ├── ComparableExtension.swift
│ ├── EnvironmentValuesExtension.swift
│ └── ViewExtension.swift
│ ├── FlexibleWidthContainer.swift
│ ├── HorizontalPanGesture.swift
│ ├── InfiniteMarkOffsetModifier.swift
│ ├── InfiniteOffsetEffect.swift
│ ├── Mechanic.swift
│ ├── Pointers.swift
│ ├── PreferenceKeys.swift
│ ├── Ruler
│ ├── Ruler.swift
│ └── RulerCell.swift
│ ├── SlidingRuler.swift
│ ├── Styling
│ ├── AnyFractionableView.swift
│ ├── AnySlidingRulerStyle.swift
│ ├── CenteredStyle
│ │ ├── BlankCenteredStyle.swift
│ │ ├── CenteredCellBody.swift
│ │ ├── CenteredScaleView.swift
│ │ └── CenteredStyle.swift
│ ├── DefaultStyle
│ │ ├── BlankStyle.swift
│ │ ├── DefaultCellBody.swift
│ │ ├── DefaultScaleView.swift
│ │ └── DefaultStyle.swift
│ ├── NativeCursorBody.swift
│ ├── Protocols
│ │ ├── FractionableView.swift
│ │ ├── MarkedRulerCellView.swift
│ │ ├── NativeMarkedRulerCellView.swift
│ │ ├── NativeRulerCellView.swift
│ │ ├── RulerCellView.swift
│ │ ├── ScaleView.swift
│ │ └── SlidingRulerStyle.swift
│ └── StyleConfiguation.swift
│ ├── Utils.swift
│ └── VSynchedTimer.swift
└── Tests
├── LinuxMain.swift
└── SlidingRulerTests
├── SlidingRulerTests.swift
└── XCTestManifests.swift
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | /*.xcodeproj
5 | xcuserdata/
6 | /SlidingRulerTestingBoard
7 | /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
8 | /Package.resolved
9 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Documentation/CustomStylingGuide.md:
--------------------------------------------------------------------------------
1 | # Custom Styling Guide
2 |
3 | WIP
4 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.2
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "SlidingRuler",
8 | platforms: [.iOS(.v13)],
9 | products: [
10 | // Products define the executables and libraries produced by a package, and make them visible to other packages.
11 | .library(
12 | name: "SlidingRuler",
13 | targets: ["SlidingRuler"]),
14 | ],
15 | dependencies: [
16 | // Dependencies declare other packages that this package depends on.
17 | // .package(url: /* package url */, from: "1.0.0"),
18 | .package(url: "https://github.com/Pyroh/SmoothOperators.git", .upToNextMajor(from: "0.4.0")),
19 | .package(url: "https://gitlab.com/Pyroh/CoreGeometry.git", .upToNextMajor(from: "4.0.0")),
20 | ],
21 | targets: [
22 | // Targets are the basic building blocks of a package. A target can define a module or a test suite.
23 | // Targets can depend on other targets in this package, and on products in packages which this package depends on.
24 | .target(
25 | name: "SlidingRuler",
26 | dependencies: ["SmoothOperators", "CoreGeometry"]),
27 | .testTarget(
28 | name: "SlidingRulerTests",
29 | dependencies: ["SlidingRuler"]),
30 | ]
31 | )
32 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |

3 |
4 |
5 | ---
6 |
7 |
13 |
21 |
22 | ---
23 | > At this time SlidingRuler shouldn't be used in production.
24 |
25 | **SlidingRuler** is a Swift package containing a SwiftUI control that acts like an linear infinite slider or a finite, more precise one. The notable difference is that the user can evaluate the value more precisely on a sliding ruler rather than on a slider.
26 | By default it shows a ruler you can slide around and a beautiful red cursor pointing toward the current value :
27 |
28 |
29 |

30 |
31 |
32 | These features are the supported features :
33 |
34 | - [x] Dynamic type
35 | - [x] Haptic feedback (on compatible devices)
36 | - [x] Light & dark color schemes
37 | - [x] Scroll inertia & rubber banding
38 | - [x] Custom styling
39 | - [x] Animations
40 | - [x] Pointer interactions
41 | - [ ] Layout direction
42 | - [ ] Accessibility
43 |
44 | It's been made to feel native and to integrate nicely in iOS and iPadOS.
45 |
46 | ## Installation
47 |
48 | ```Text
49 | dependencies: [
50 | // Dependencies declare other packages that this package depends on.
51 | .package(url: "https://github.com/Pyroh/SlidingRuler", .upToNextMajor(from: "0.1.0")),
52 | ],
53 | ```
54 |
55 | ## Usage
56 | > Before using anything be sure to `import SlidingRuler` in the target swift file.
57 |
58 | Like any SwiftUI control you can create a `SlidingRuler` with an unique parameter: the value.
59 | Like any SwiftUI input control the value is a `Binding<...>` :
60 |
61 | ```Swift
62 | @State private var value: Double = 0
63 |
64 | var body: some View {
65 | ...
66 | SlidingRuler(value: $value)
67 | ...
68 | }
69 | ```
70 |
71 | Note that `value` must conform to `BinaryFloatingPoint`.
72 |
73 | ### ✅ When to use ?
74 | It's good to use a sliding ruler in these cases:
75 |
76 | - To input a numeric value that belongs to an unlimited range or a particularly large one.
77 | - To input measurable values like masses or lenghts.
78 | - To pick a precise value in a tiny range —for this use a small `step` value.
79 | - You're already using multiple sliding rulers in your form and using a slider for this value will break the continuity. —*Ok, but read the next section first.*
80 | - You just feel like to and you're confident it'll be ok. —*Ok, but read the next section first.*
81 |
82 | Additionaly a disabled slinding ruler can be used as a meter.
83 |
84 | ### ⛔️ When not to use
85 | It's bad to use a sliding ruler in these cases:
86 |
87 | - To make the user chose between a small to medium set of discrete values. → *Use a `Picker` or a `Stepper`.*
88 | - To pick an unprecise value in a small closed range. → *Use a `Slider`.*
89 | - To change a device audio volume. → *Use a `Slider`.*
90 | - To let the user input an arbitrary value like its age. → *Use a `TextField`. Perhaps UI/UX design is not for you after all...*
91 | - To input a date component. → Use a `DatePicker`. Are you out of your mind ?
92 |
93 | ### Using finite or semi-finite ranges
94 | In some cases you may want to use such ranges when it makes sense —particularly when inputing strictly positive or negative values.
95 | A slinding ruler will show these boundaries clearly to the user :
96 |
97 |
98 |
99 |
100 |
101 | The user is not allowed to drag the ruler above these boudaries. Trying so will result in an haptic feedback (on compatible devices) and the over drag will feel like a rubber band, like a scroll view.
102 |
103 | ### Methods added to `View`
104 | `SlidingRuler` don't have no direct method but like many SwiftUI controls it adds some methods to `View`. They work in the same fashion as other `View` methods that impact a component and all its descendent in a view tree.
105 |
106 | #### `slidingRulerStyle`
107 |
108 | ```Swift
109 | func slidingRulerStyle(_ style: S) -> some View where S: SlidingRulerStyle
110 | ```
111 | Sets the style for all sliding rulers within the view tree. See the [Custom Styling Guide](./Documentation/CustomStylingGuide.md) (once it's been written).
112 |
113 | #### `slidingRulerCellOverflow`
114 |
115 | ```Swift
116 | func slidingRulerCellOverflow(_ overflow: Int) -> some View
117 | ```
118 | Sets the cell overflow for all sliding rulers within the view tree. See the [Custom Styling Guide](./Documentation/CustomStylingGuide.md) (once it's been written).
119 | *You may get retired without even using this method, ever.*
120 |
121 | ### Parameter list
122 | The complete `init` method signature is :
123 |
124 | ```Swift
125 | init(value: Binding,
126 | in bounds: ClosedRange = -V.infinity...V.infinity,
127 | step: V.Stride = 1,
128 | snap: Mark = .none,
129 | tick: Mark = .none,
130 | onEditingChanged: @escaping (Bool) -> () = { _ in },
131 | formatter: NumberFormatter? = nil)
132 | ```
133 |
134 | #### `bounds` : *`ClosedRange`*
135 | The closed range of possible values.
136 | By default it is `-V.infinity...V.infinity`. Meaning that the sliding ruler is virtualy infinite.
137 |
138 | #### `step` : *`V.Stride`*
139 | The stride of the SlidingRuler.
140 | By default it is `1.0`.
141 |
142 | #### `snap` : *`Mark`*
143 | Possible values : `.none`, `.unit`, `.half`, `.fraction`.
144 | Describes the ruler's marks stickyness: when the ruler stops and the cursor is near a graduation it will snap to it.
145 |
146 | - `.none`: no snap.
147 | - `.unit`: snaps on every whole unit graduations.
148 | - `.half`: snaps on every whle unit and half unit graduations.
149 | - `.fraction`: snaps on every graduations.
150 |
151 | By default it is `.none`.
152 |
153 | Note: to trigger a snap the cursor must be _near_ the graduation. Here _near_ means that the delta between the cursor and the graduation is strictly less than a fraction of the ruler unit.
154 | The value of a fraction is driven by the style's `fractions` property. The default styles have a `fractions` property equal to `10` so a fraction equals to `1/10` of a unit or `0.1` with the default `step` (`1.0`).
155 |
156 | #### `tick` : *`Mark`*
157 | Possible values : `.none`, `.unit`, `.half`, `.fraction`.
158 | Defines what kind of graduation produces an haptic feedback when reached.
159 |
160 | - `.none`: no haptic feedback.
161 | - `.unit`: haptic feedbak on every whole unit graduations.
162 | - `.half`: haptic feedbak on every whole unit and half unit graduations. (If the style's fraction count allows an half)
163 | - `.fraction`: haptic feedbak on every graduations.
164 |
165 | By default it is `.none`.
166 |
167 | #### `onEditingChanged` : *`(Bool) -> Void`*
168 | A closure executed when a drag session happens. It receives a boolean value set to `true` when the drag session starts and `false` when the value stops changing.
169 | By default it is an empty closure that produces no action.
170 |
171 | #### `formatter` : *`NumberFormatter`*
172 | A `NumberFormatter` instance the ruler uses to format the ruler's marks.
173 | By default it is `nil`.
174 |
175 | ### Slinding ruler styles
176 | > For a comprehensive custom styling documentation See the [Custom Styling Guide](./Documentation/CustomStylingGuide.md) (once it's been written).
177 | > Custom styling is still a work in progress. As it is tied to accessibility some work on this topic is still required to determine how a style should adapt to it.
178 |
179 | By default `SlindingRuler` ships with four styles. Two of them don't show any mark on the ruler
180 |
181 | #### `PrimarySlidingRulerStyle`
182 | > This is the default style.
183 |
184 |
185 |
186 | #### `CenteredSlindingRulerStyle`
187 |
188 |
189 |
190 | #### `BlankSlidingRulerStyle`
191 |
192 |
193 |
194 | #### `BlankCenteredSlidingRulerStyle`
195 |
196 |
197 |
198 |
199 | ### Example
200 | #### Percentage value
201 | A SlindingRuler that goes from 0 to 100%, that snaps and gives haptic feedback on any graduation.
202 |
203 |
204 |
205 | ```Swift
206 | struct PercentSlidingRuler: View {
207 | @State private var value: Double = .zero
208 |
209 | private var formatter: NumberFormatter {
210 | let f = NumberFormatter()
211 | f.numberStyle = .percent
212 | f.maximumFractionDigits = 0
213 | return f
214 | }
215 |
216 | var body: some View {
217 | SlidingRuler(value: $value,
218 | in: 0...1,
219 | step: 0.1,
220 | snap: .fraction,
221 | tick: .fraction,
222 | formatter: formatter)
223 | }
224 | }
225 | ```
226 |
227 | ### License
228 | See LICENSE
229 |
--------------------------------------------------------------------------------
/Resources/Idle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Pyroh/SlidingRuler/707b15e0ade64b488e7146a487cc5ee9570af9fa/Resources/Idle.png
--------------------------------------------------------------------------------
/Resources/Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Pyroh/SlidingRuler/707b15e0ade64b488e7146a487cc5ee9570af9fa/Resources/Logo.png
--------------------------------------------------------------------------------
/Resources/Styles/Blank.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Pyroh/SlidingRuler/707b15e0ade64b488e7146a487cc5ee9570af9fa/Resources/Styles/Blank.png
--------------------------------------------------------------------------------
/Resources/Styles/BlankCentered.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Pyroh/SlidingRuler/707b15e0ade64b488e7146a487cc5ee9570af9fa/Resources/Styles/BlankCentered.png
--------------------------------------------------------------------------------
/Resources/Styles/Centered.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Pyroh/SlidingRuler/707b15e0ade64b488e7146a487cc5ee9570af9fa/Resources/Styles/Centered.png
--------------------------------------------------------------------------------
/Resources/Styles/Primary.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Pyroh/SlidingRuler/707b15e0ade64b488e7146a487cc5ee9570af9fa/Resources/Styles/Primary.png
--------------------------------------------------------------------------------
/Resources/example_percentage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Pyroh/SlidingRuler/707b15e0ade64b488e7146a487cc5ee9570af9fa/Resources/example_percentage.png
--------------------------------------------------------------------------------
/Resources/lowerbound.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Pyroh/SlidingRuler/707b15e0ade64b488e7146a487cc5ee9570af9fa/Resources/lowerbound.png
--------------------------------------------------------------------------------
/Resources/upperbound.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Pyroh/SlidingRuler/707b15e0ade64b488e7146a487cc5ee9570af9fa/Resources/upperbound.png
--------------------------------------------------------------------------------
/Sources/SlidingRuler/Enums/SlidingRulerState.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SlidingRulerState.swift
3 | //
4 | // SlidingRuler
5 | //
6 | // MIT License
7 | //
8 | // Copyright (c) 2020 Pierre Tacchi
9 | //
10 | // Permission is hereby granted, free of charge, to any person obtaining a copy
11 | // of this software and associated documentation files (the "Software"), to deal
12 | // in the Software without restriction, including without limitation the rights
13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14 | // copies of the Software, and to permit persons to whom the Software is
15 | // furnished to do so, subject to the following conditions:
16 | //
17 | // The above copyright notice and this permission notice shall be included in all
18 | // copies or substantial portions of the Software.
19 | //
20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26 | // SOFTWARE.
27 | //
28 |
29 |
30 | enum SlidingRulerState {
31 | case idle
32 | case dragging
33 | case animating
34 | case flicking
35 | case springing
36 | case stoppedFlick
37 | case stoppedSpring
38 | }
39 |
--------------------------------------------------------------------------------
/Sources/SlidingRuler/Enums/StickyMark.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StickyMark.swift
3 | //
4 | // SlidingRuler
5 | //
6 | // MIT License
7 | //
8 | // Copyright (c) 2020 Pierre Tacchi
9 | //
10 | // Permission is hereby granted, free of charge, to any person obtaining a copy
11 | // of this software and associated documentation files (the "Software"), to deal
12 | // in the Software without restriction, including without limitation the rights
13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14 | // copies of the Software, and to permit persons to whom the Software is
15 | // furnished to do so, subject to the following conditions:
16 | //
17 | // The above copyright notice and this permission notice shall be included in all
18 | // copies or substantial portions of the Software.
19 | //
20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26 | // SOFTWARE.
27 | //
28 |
29 |
30 | public enum StickyMark {
31 | public enum Bound { case any, unit, half, tenth }
32 |
33 | case none, nearest, lower(Bound), upper(Bound)
34 |
35 | var bound: Bound {
36 | switch self {
37 | case .none, .nearest:
38 | return .any
39 | case .lower(let b), .upper(let b):
40 | return b
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/Sources/SlidingRuler/Enums/Tick.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Tick.swift
3 | //
4 | // SlidingRuler
5 | //
6 | // MIT License
7 | //
8 | // Copyright (c) 2020 Pierre Tacchi
9 | //
10 | // Permission is hereby granted, free of charge, to any person obtaining a copy
11 | // of this software and associated documentation files (the "Software"), to deal
12 | // in the Software without restriction, including without limitation the rights
13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14 | // copies of the Software, and to permit persons to whom the Software is
15 | // furnished to do so, subject to the following conditions:
16 | //
17 | // The above copyright notice and this permission notice shall be included in all
18 | // copies or substantial portions of the Software.
19 | //
20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26 | // SOFTWARE.
27 | //
28 |
29 | public enum Mark {
30 | case none, unit, half, fraction
31 | }
32 |
33 | public enum Tick {
34 | case none, unit, half, tenth
35 |
36 | var coeff: Double {
37 | switch self {
38 | case .none: return .nan
39 | case .unit: return 1
40 | case .half: return 0.5
41 | case .tenth: return 0.1
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/Sources/SlidingRuler/Extensions/BinaryFloatingPointExtension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BinaryFloatingPointExtension.swift
3 | //
4 | // SlidingRuler
5 | //
6 | // MIT License
7 | //
8 | // Copyright (c) 2020 Pierre Tacchi
9 | //
10 | // Permission is hereby granted, free of charge, to any person obtaining a copy
11 | // of this software and associated documentation files (the "Software"), to deal
12 | // in the Software without restriction, including without limitation the rights
13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14 | // copies of the Software, and to permit persons to whom the Software is
15 | // furnished to do so, subject to the following conditions:
16 | //
17 | // The above copyright notice and this permission notice shall be included in all
18 | // copies or substantial portions of the Software.
19 | //
20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26 | // SOFTWARE.
27 | //
28 |
29 |
30 | import Foundation
31 |
32 | extension BinaryFloatingPoint {
33 | mutating func approximate() { self = approximated() }
34 | func approximated() -> Self { (self * 1_000_000).rounded() / 1_000_000 }
35 |
36 | func nearestBound(of range: ClosedRange, equality: Bool = false) -> Self {
37 | let deltaLow = self - range.lowerBound
38 | let deltaUp = range.upperBound - self
39 |
40 | if deltaUp == deltaLow {
41 | return !equality ? range.lowerBound : range.upperBound
42 | } else {
43 | return deltaLow < deltaUp ? range.lowerBound : range.upperBound
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/Sources/SlidingRuler/Extensions/BinaryIntegerExtension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BinaryIntegerExtension.swift
3 | //
4 | // SlidingRuler
5 | //
6 | // MIT License
7 | //
8 | // Copyright (c) 2020 Pierre Tacchi
9 | //
10 | // Permission is hereby granted, free of charge, to any person obtaining a copy
11 | // of this software and associated documentation files (the "Software"), to deal
12 | // in the Software without restriction, including without limitation the rights
13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14 | // copies of the Software, and to permit persons to whom the Software is
15 | // furnished to do so, subject to the following conditions:
16 | //
17 | // The above copyright notice and this permission notice shall be included in all
18 | // copies or substantial portions of the Software.
19 | //
20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26 | // SOFTWARE.
27 | //
28 |
29 |
30 | import SmoothOperators
31 |
32 | extension BinaryInteger {
33 | @inlinable var isEven: Bool { self.isMultiple(of: 2) }
34 |
35 | @inlinable func nextOdd() -> Self { isEven ? self+ : self }
36 | @inlinable func previousOdd() -> Self { isEven ? self- : self }
37 |
38 | @inlinable func nextEven() -> Self { isEven ? self : self+ }
39 | @inlinable func previousEven() -> Self { isEven ? self : self- }
40 | }
41 |
--------------------------------------------------------------------------------
/Sources/SlidingRuler/Extensions/ClosedRangeExtension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ClosedRangeExtension.swift
3 | //
4 | // SlidingRulerTestingBoard
5 | //
6 | // MIT License
7 | //
8 | // Copyright (c) 2020 Pierre Tacchi
9 | //
10 | // Permission is hereby granted, free of charge, to any person obtaining a copy
11 | // of this software and associated documentation files (the "Software"), to deal
12 | // in the Software without restriction, including without limitation the rights
13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14 | // copies of the Software, and to permit persons to whom the Software is
15 | // furnished to do so, subject to the following conditions:
16 | //
17 | // The above copyright notice and this permission notice shall be included in all
18 | // copies or substantial portions of the Software.
19 | //
20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26 | // SOFTWARE.
27 | //
28 |
29 |
30 | extension ClosedRange {
31 | func contains(_ range: Self) -> Bool {
32 | self.contains(range.lowerBound) && self.contains(range.upperBound)
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/Sources/SlidingRuler/Extensions/ComparableExtension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ComparableExtension.swift
3 | //
4 | // SlidingRuler
5 | //
6 | // MIT License
7 | //
8 | // Copyright (c) 2020 Pierre Tacchi
9 | //
10 | // Permission is hereby granted, free of charge, to any person obtaining a copy
11 | // of this software and associated documentation files (the "Software"), to deal
12 | // in the Software without restriction, including without limitation the rights
13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14 | // copies of the Software, and to permit persons to whom the Software is
15 | // furnished to do so, subject to the following conditions:
16 | //
17 | // The above copyright notice and this permission notice shall be included in all
18 | // copies or substantial portions of the Software.
19 | //
20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26 | // SOFTWARE.
27 | //
28 |
29 |
30 | import Foundation
31 |
32 |
33 |
34 | extension Comparable {
35 | static func clamp(_ x: Self, _ min: Self, _ max: Self) -> Self {
36 | Swift.min(Swift.max(x, min), max)
37 | }
38 |
39 | func clamped(to min: Self, _ max: Self) -> Self {
40 | Self.clamp(self, min, max)
41 | }
42 |
43 | mutating func clamp(to min: Self, _ max: Self) {
44 | self = Self.clamp(self, min, max)
45 | }
46 |
47 | func clamped(to range: ClosedRange) -> Self {
48 | Self.clamp(self, range.lowerBound, range.upperBound)
49 | }
50 |
51 | mutating func clamp(to range: ClosedRange) {
52 | self = self.clamped(to: range)
53 | }
54 |
55 | func isBound(of range: ClosedRange) -> Bool {
56 | range.lowerBound == self || range.upperBound == self
57 | }
58 | }
59 |
60 |
--------------------------------------------------------------------------------
/Sources/SlidingRuler/Extensions/EnvironmentValuesExtension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EnvironmentValuesExtension.swift
3 | //
4 | // SlidingRuler
5 | //
6 | // MIT License
7 | //
8 | // Copyright (c) 2020 Pierre Tacchi
9 | //
10 | // Permission is hereby granted, free of charge, to any person obtaining a copy
11 | // of this software and associated documentation files (the "Software"), to deal
12 | // in the Software without restriction, including without limitation the rights
13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14 | // copies of the Software, and to permit persons to whom the Software is
15 | // furnished to do so, subject to the following conditions:
16 | //
17 | // The above copyright notice and this permission notice shall be included in all
18 | // copies or substantial portions of the Software.
19 | //
20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26 | // SOFTWARE.
27 | //
28 |
29 |
30 | import SwiftUI
31 |
32 | enum StaticSlidingRulerStyleEnvironment {
33 | @Environment(\.slidingRulerStyle.cellWidth) static var cellWidth
34 | @Environment(\.slidingRulerStyle.cursorAlignment) static var alignment
35 | @Environment(\.slidingRulerStyle.hasMarks) static var hasMarks
36 | }
37 |
38 | struct SlidingRulerStyleEnvironmentKey: EnvironmentKey {
39 | static var defaultValue: AnySlidingRulerStyle { .init(style: PrimarySlidingRulerStyle()) }
40 | }
41 |
42 | struct SlideRulerCellOverflow: EnvironmentKey {
43 | static var defaultValue: Int { 3 }
44 | }
45 |
46 | extension EnvironmentValues {
47 | var slidingRulerStyle: AnySlidingRulerStyle {
48 | get { self[SlidingRulerStyleEnvironmentKey.self] }
49 | set { self[SlidingRulerStyleEnvironmentKey.self] = newValue }
50 | }
51 |
52 | var slidingRulerCellOverflow: Int {
53 | get { self[SlideRulerCellOverflow.self] }
54 | set { self[SlideRulerCellOverflow.self] = newValue }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/Sources/SlidingRuler/Extensions/ViewExtension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewExtension.swift
3 | //
4 | // SlidingRuler
5 | //
6 | // MIT License
7 | //
8 | // Copyright (c) 2020 Pierre Tacchi
9 | //
10 | // Permission is hereby granted, free of charge, to any person obtaining a copy
11 | // of this software and associated documentation files (the "Software"), to deal
12 | // in the Software without restriction, including without limitation the rights
13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14 | // copies of the Software, and to permit persons to whom the Software is
15 | // furnished to do so, subject to the following conditions:
16 | //
17 | // The above copyright notice and this permission notice shall be included in all
18 | // copies or substantial portions of the Software.
19 | //
20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26 | // SOFTWARE.
27 | //
28 |
29 |
30 | import SwiftUI
31 |
32 | extension View {
33 | public func slidingRulerStyle(_ style: S) -> some View where S: SlidingRulerStyle {
34 | environment(\.slidingRulerStyle, .init(style: style))
35 | }
36 |
37 | public func slidingRulerCellOverflow(_ overflow: Int) -> some View {
38 | environment(\.slidingRulerCellOverflow, overflow)
39 | }
40 | }
41 |
42 | extension View {
43 | func frame(size: CGSize?, alignment: Alignment = .center) -> some View {
44 | self.frame(width: size?.width, height: size?.height, alignment: alignment)
45 | }
46 |
47 | func onPreferenceChange(_ key: K.Type,
48 | storeValueIn storage: Binding,
49 | action: (() -> ())? = nil ) -> some View where K.Value: Equatable {
50 | onPreferenceChange(key, perform: {
51 | storage.wrappedValue = $0
52 | action?()
53 | })
54 | }
55 |
56 | func propagateHeight(_ key: K.Type, transform: @escaping (K.Value) -> K.Value = { $0 }) -> some View where K.Value == CGFloat? {
57 | overlay(
58 | GeometryReader { proxy in
59 | Color.clear
60 | .preference(key: key, value: transform(proxy.size.height))
61 | }
62 | )
63 | }
64 |
65 | func propagateWidth(_ key: K.Type, transform: @escaping (K.Value) -> K.Value = { $0 }) -> some View where K.Value == CGFloat? {
66 | overlay(
67 | GeometryReader { proxy in
68 | Color.clear
69 | .preference(key: key, value: transform(proxy.size.width))
70 | }
71 | )
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/Sources/SlidingRuler/FlexibleWidthContainer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SwiftUIView.swift
3 | //
4 | // SlidingRuler
5 | //
6 | // MIT License
7 | //
8 | // Copyright (c) 2020 Pierre Tacchi
9 | //
10 | // Permission is hereby granted, free of charge, to any person obtaining a copy
11 | // of this software and associated documentation files (the "Software"), to deal
12 | // in the Software without restriction, including without limitation the rights
13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14 | // copies of the Software, and to permit persons to whom the Software is
15 | // furnished to do so, subject to the following conditions:
16 | //
17 | // The above copyright notice and this permission notice shall be included in all
18 | // copies or substantial portions of the Software.
19 | //
20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26 | // SOFTWARE.
27 | //
28 |
29 |
30 | import SwiftUI
31 |
32 | private struct _FlexibleWidthContainerHeightPreferenceKey: PreferenceKey {
33 | static func reduce(value: inout CGFloat?, nextValue: () -> CGFloat?) {
34 | if let newValue = nextValue() {
35 | value = newValue
36 | }
37 | }
38 | }
39 |
40 | struct FlexibleWidthContainer: View {
41 | @State private var height: CGFloat?
42 | private let content: Content
43 |
44 | init(@ViewBuilder content: @escaping () -> Content) {
45 | self.content = content()
46 | }
47 |
48 | var body: some View {
49 | Color.clear
50 | .frame(height: height)
51 | .overlay(content.propagateHeight(_FlexibleWidthContainerHeightPreferenceKey.self))
52 | .onPreferenceChange(_FlexibleWidthContainerHeightPreferenceKey.self, storeValueIn: $height)
53 | .clipped()
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/Sources/SlidingRuler/HorizontalPanGesture.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HorizontalPanGesture.swift
3 | //
4 | // SlidingRuler
5 | //
6 | // MIT License
7 | //
8 | // Copyright (c) 2020 Pierre Tacchi
9 | //
10 | // Permission is hereby granted, free of charge, to any person obtaining a copy
11 | // of this software and associated documentation files (the "Software"), to deal
12 | // in the Software without restriction, including without limitation the rights
13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14 | // copies of the Software, and to permit persons to whom the Software is
15 | // furnished to do so, subject to the following conditions:
16 | //
17 | // The above copyright notice and this permission notice shall be included in all
18 | // copies or substantial portions of the Software.
19 | //
20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26 | // SOFTWARE.
27 | //
28 |
29 |
30 | import SwiftUI
31 | import CoreGeometry
32 |
33 | struct HorizontalDragGestureValue {
34 | let state: UIGestureRecognizer.State
35 | let translation: CGSize
36 | let velocity: CGFloat
37 | let startLocation: CGPoint
38 | let location: CGPoint
39 | }
40 |
41 | protocol HorizontalPanGestureReceiverViewDelegate: class {
42 | func viewTouchedWithoutPan(_ view: UIView)
43 | }
44 |
45 | class HorizontalPanGestureReceiverView: UIView {
46 | weak var delegate: HorizontalPanGestureReceiverViewDelegate?
47 |
48 | override func touchesEnded(_ touches: Set, with event: UIEvent?) {
49 | super.touchesEnded(touches, with: event)
50 | delegate?.viewTouchedWithoutPan(self)
51 | }
52 | }
53 |
54 | extension View {
55 | func onHorizontalDragGesture(initialTouch: @escaping () -> () = { },
56 | prematureEnd: @escaping () -> () = { },
57 | perform action: @escaping (HorizontalDragGestureValue) -> ()) -> some View {
58 | self.overlay(HorizontalPanGesture(beginTouch: initialTouch, prematureEnd: prematureEnd, action: action))
59 | }
60 | }
61 |
62 | private struct HorizontalPanGesture: UIViewRepresentable {
63 | typealias Action = (HorizontalDragGestureValue) -> ()
64 |
65 | class Coordinator: NSObject, UIGestureRecognizerDelegate, HorizontalPanGestureReceiverViewDelegate {
66 | private let beginTouch: () -> ()
67 | private let prematureEnd: () -> ()
68 | private let action: Action
69 | weak var view: UIView?
70 |
71 | init(_ beginTouch: @escaping () -> () = { }, _ prematureEnd: @escaping () -> () = { }, _ action: @escaping Action) {
72 | self.beginTouch = beginTouch
73 | self.prematureEnd = prematureEnd
74 | self.action = action
75 | }
76 |
77 | @objc func panGestureHandler(_ gesture: UIPanGestureRecognizer) {
78 | let translation = gesture.translation(in: view)
79 | let velocity = gesture.velocity(in: view)
80 | let location = gesture.location(in: view)
81 | let startLocation = location - translation
82 |
83 | let value = HorizontalDragGestureValue(state: gesture.state,
84 | translation: .init(horizontal: translation.x),
85 | velocity: velocity.x,
86 | startLocation: startLocation,
87 | location: location)
88 | self.action(value)
89 | }
90 |
91 | func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
92 | guard let pgr = gestureRecognizer as? UIPanGestureRecognizer else { return false }
93 | let velocity = pgr.velocity(in: view)
94 | return abs(velocity.x) > abs(velocity.y)
95 | }
96 |
97 | func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive event: UIEvent) -> Bool {
98 | beginTouch()
99 | return true
100 | }
101 |
102 | func viewTouchedWithoutPan(_ view: UIView) {
103 | prematureEnd()
104 | }
105 | }
106 |
107 | @Environment(\.slidingRulerStyle) private var style
108 |
109 | let beginTouch: () -> ()
110 | let prematureEnd: () -> ()
111 | let action: Action
112 |
113 | func makeCoordinator() -> Coordinator {
114 | .init(beginTouch, prematureEnd, action)
115 | }
116 |
117 | func makeUIView(context: Context) -> UIView {
118 | let view = HorizontalPanGestureReceiverView(frame: .init(size: .init(square: 42)))
119 | let pgr = UIPanGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.panGestureHandler(_:)))
120 | view.delegate = context.coordinator
121 | pgr.delegate = context.coordinator
122 | view.addGestureRecognizer(pgr)
123 | context.coordinator.view = view
124 |
125 | // Pointer interactions
126 | if #available(iOS 13.4, *), style.supportsPointerInteraction {
127 | pgr.allowedScrollTypesMask = .continuous
128 | view.addInteraction(UIPointerInteraction(delegate: context.coordinator))
129 | }
130 |
131 | return view
132 | }
133 |
134 | func updateUIView(_ uiView: UIView, context: Context) { }
135 | }
136 |
137 | @available(iOS 13.4, *)
138 | extension HorizontalPanGesture.Coordinator: UIPointerInteractionDelegate {
139 | func pointerInteraction(_ interaction: UIPointerInteraction, styleFor region: UIPointerRegion) -> UIPointerStyle? {
140 | .init(shape: .path(Pointers.standard), constrainedAxes: .vertical)
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/Sources/SlidingRuler/InfiniteMarkOffsetModifier.swift:
--------------------------------------------------------------------------------
1 | //
2 | // InfiniteMarkOffsetModifier.swift
3 | //
4 | // SlidingRuler
5 | //
6 | // MIT License
7 | //
8 | // Copyright (c) 2020 Pierre Tacchi
9 | //
10 | // Permission is hereby granted, free of charge, to any person obtaining a copy
11 | // of this software and associated documentation files (the "Software"), to deal
12 | // in the Software without restriction, including without limitation the rights
13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14 | // copies of the Software, and to permit persons to whom the Software is
15 | // furnished to do so, subject to the following conditions:
16 | //
17 | // The above copyright notice and this permission notice shall be included in all
18 | // copies or substantial portions of the Software.
19 | //
20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26 | // SOFTWARE.
27 | //
28 |
29 |
30 | import SwiftUI
31 |
32 | struct InfiniteMarkOffsetModifier: AnimatableModifier {
33 | @Environment(\.slidingRulerCellOverflow) private var overflow
34 |
35 | var value: CGFloat
36 | let step: CGFloat
37 |
38 | var offset: CGFloat {
39 | let overflow = CGFloat(self.overflow)
40 | return (value / step / overflow).approximated().rounded(.towardZero) * overflow
41 | }
42 |
43 | var animatableData: CGFloat {
44 | get { value }
45 | set { value = newValue }
46 | }
47 |
48 | init(_ value: CGFloat, step: CGFloat) {
49 | self.value = value
50 | self.step = step
51 | }
52 |
53 | func body(content: Content) -> some View {
54 | content.preference(key: MarkOffsetPreferenceKey.self, value: offset)
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/Sources/SlidingRuler/InfiniteOffsetEffect.swift:
--------------------------------------------------------------------------------
1 | //
2 | // InfiniteOffsetEffect.swift
3 | //
4 | // SlidingRuler
5 | //
6 | // MIT License
7 | //
8 | // Copyright (c) 2020 Pierre Tacchi
9 | //
10 | // Permission is hereby granted, free of charge, to any person obtaining a copy
11 | // of this software and associated documentation files (the "Software"), to deal
12 | // in the Software without restriction, including without limitation the rights
13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14 | // copies of the Software, and to permit persons to whom the Software is
15 | // furnished to do so, subject to the following conditions:
16 | //
17 | // The above copyright notice and this permission notice shall be included in all
18 | // copies or substantial portions of the Software.
19 | //
20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26 | // SOFTWARE.
27 | //
28 |
29 |
30 | import SwiftUI
31 |
32 | struct InfiniteOffsetEffect: GeometryEffect {
33 | var offset: CGSize
34 | let maxOffset: CGFloat
35 |
36 | var correctedOffset: CGSize {
37 | let tx = offset.width.truncatingRemainder(dividingBy: maxOffset)
38 | return .init(horizontal: tx)
39 | }
40 |
41 | var animatableData: CGSize.AnimatableData {
42 | get { offset.animatableData }
43 | set { offset.animatableData = newValue }
44 | }
45 |
46 | func effectValue(size: CGSize) -> ProjectionTransform {
47 | assert(!size.width.isNaN && !size.height.isNaN)
48 | return ProjectionTransform(CGAffineTransform(translationX: correctedOffset.width, y: correctedOffset.height))
49 | }
50 | }
51 |
52 |
--------------------------------------------------------------------------------
/Sources/SlidingRuler/Mechanic.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Mechanic.swift
3 | //
4 | // SlidingRuler
5 | //
6 | // MIT License
7 | //
8 | // Copyright (c) 2020 Pierre Tacchi
9 | //
10 | // Permission is hereby granted, free of charge, to any person obtaining a copy
11 | // of this software and associated documentation files (the "Software"), to deal
12 | // in the Software without restriction, including without limitation the rights
13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14 | // copies of the Software, and to permit persons to whom the Software is
15 | // furnished to do so, subject to the following conditions:
16 | //
17 | // The above copyright notice and this permission notice shall be included in all
18 | // copies or substantial portions of the Software.
19 | //
20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26 | // SOFTWARE.
27 | //
28 |
29 | import UIKit.UIScrollView
30 | import CoreGraphics
31 |
32 | enum Mechanic {
33 |
34 | enum Inertia {
35 | private static let epsilon: CGFloat = 0.6
36 |
37 | /// Velocity at time `t` of the initial velocity `v0` decelerated by the given deceleration rate.
38 | static func velocity(atTime t: TimeInterval, v0: CGFloat, decelerationRate rate: UIScrollView.DecelerationRate) -> CGFloat {
39 | v0 * pow(rate.rawValue, (1000 * CGFloat(t)))
40 | }
41 |
42 | /// Travelled distance at time `t` for the initial velocity `v0` decelerated by the given deceleration rate.
43 | static func distance(atTime t: TimeInterval, v0: CGFloat, decelerationRate rate: UIScrollView.DecelerationRate) -> CGFloat {
44 | v0 * (pow(rate.rawValue, 1000 * CGFloat(t)) - 1) / (coef(rate))
45 | }
46 |
47 | /// Total distance travelled for he initial velocity `v0` decelerated by the given deceleration rate before being completely still.
48 | static func totalDistance(forVelocity v0: CGFloat, decelerationRate rate: UIScrollView.DecelerationRate) -> CGFloat {
49 | distance(atTime: duration(forVelocity: v0, decelerationRate: rate), v0: v0, decelerationRate: rate)
50 | }
51 |
52 | /// Total time ellapsed before the motion become completely still for the initial velocity `v0` decelerated by the given deceleration rate.
53 | static func duration(forVelocity v0: CGFloat, decelerationRate rate: UIScrollView.DecelerationRate) -> TimeInterval {
54 | TimeInterval((log((-1000 * epsilon * log(rate.rawValue)) / abs(v0))) / coef(rate))
55 | }
56 |
57 | static func time(toReachDistance x: CGFloat, forVelocity v0: CGFloat, decelerationRate rate: UIScrollView.DecelerationRate) -> TimeInterval {
58 | TimeInterval(log(1 + coef(rate) * x / v0) / coef(rate))
59 | }
60 |
61 | static func coef(_ rate: UIScrollView.DecelerationRate) -> CGFloat {
62 | 1000 * log(rate.rawValue)
63 | }
64 | }
65 |
66 | enum Spring {
67 | private static var e: CGFloat { CGFloat(M_E) }
68 | private static var threshold: CGFloat { 0.25 }
69 |
70 | private static var stiffness: CGFloat { 100 }
71 | private static var damping: CGFloat { 2 * sqrt(stiffness) }
72 | private static var beta: CGFloat { sqrt(stiffness) }
73 |
74 | static func duration(forVelocity v0: CGFloat, displacement c1: CGFloat) -> TimeInterval {
75 | guard v0 + c1 != 0 else { return .zero }
76 |
77 | let c2 = v0 + beta * c1
78 |
79 | let t1 = 1 / beta * log(2 * c1 / threshold)
80 | let t2 = 2 / beta * log(4 * c2 / (e * beta * threshold))
81 |
82 | return TimeInterval(max(t1, t2))
83 | }
84 |
85 | static func value(atTime t: TimeInterval, v0: CGFloat, displacement c1: CGFloat) -> CGFloat {
86 | let c2 = v0 + beta * c1
87 |
88 | return exp(-beta * CGFloat(t)) * (c1 + c2 * CGFloat(t))
89 | }
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/Sources/SlidingRuler/Pointers.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Pointers.swift
3 | //
4 | // SlidingRuler
5 | //
6 | // MIT License
7 | //
8 | // Copyright (c) 2020 Pierre Tacchi
9 | //
10 | // Permission is hereby granted, free of charge, to any person obtaining a copy
11 | // of this software and associated documentation files (the "Software"), to deal
12 | // in the Software without restriction, including without limitation the rights
13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14 | // copies of the Software, and to permit persons to whom the Software is
15 | // furnished to do so, subject to the following conditions:
16 | //
17 | // The above copyright notice and this permission notice shall be included in all
18 | // copies or substantial portions of the Software.
19 | //
20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26 | // SOFTWARE.
27 | //
28 |
29 |
30 | import UIKit.UIBezierPath
31 |
32 | enum Pointers {
33 | static var standard: UIBezierPath {
34 | let path = UIBezierPath()
35 |
36 | path.move(to: CGPoint(x: 18.78348, y: 1.134168))
37 | path.addCurve(to: CGPoint(x: 19, y: 2.051366), controlPoint1: CGPoint(x: 18.925869, y: 1.418949), controlPoint2: CGPoint(x: 19, y: 1.732971))
38 | path.addLine(to: CGPoint(x: 19, y: 16.949083))
39 | path.addCurve(to: CGPoint(x: 16.949083, y: 19), controlPoint1: CGPoint(x: 19, y: 18.081774), controlPoint2: CGPoint(x: 18.081774, y: 19))
40 | path.addCurve(to: CGPoint(x: 16.031885, y: 18.78348), controlPoint1: CGPoint(x: 16.63069, y: 19), controlPoint2: CGPoint(x: 16.316668, y: 18.925869))
41 | path.addLine(to: CGPoint(x: 1.134168, y: 11.33462))
42 | path.addCurve(to: CGPoint(x: 0.21697, y: 8.583027), controlPoint1: CGPoint(x: 0.121059, y: 10.828066), controlPoint2: CGPoint(x: -0.289584, y: 9.596135))
43 | path.addCurve(to: CGPoint(x: 1.134168, y: 7.665829), controlPoint1: CGPoint(x: 0.415425, y: 8.186118), controlPoint2: CGPoint(x: 0.737259, y: 7.864284))
44 | path.addLine(to: CGPoint(x: 16.031885, y: 0.21697))
45 | path.addCurve(to: CGPoint(x: 18.78348, y: 1.134168), controlPoint1: CGPoint(x: 17.044994, y: -0.289584), controlPoint2: CGPoint(x: 18.276924, y: 0.121059))
46 | path.close()
47 | path.move(to: CGPoint(x: 30.21652, y: 1.134168))
48 | path.addCurve(to: CGPoint(x: 32.968113, y: 0.21697), controlPoint1: CGPoint(x: 30.723076, y: 0.121059), controlPoint2: CGPoint(x: 31.955006, y: -0.289584))
49 | path.addLine(to: CGPoint(x: 32.968113, y: 0.21697))
50 | path.addLine(to: CGPoint(x: 47.865833, y: 7.665829))
51 | path.addCurve(to: CGPoint(x: 48.783031, y: 8.583027), controlPoint1: CGPoint(x: 48.262741, y: 7.864284), controlPoint2: CGPoint(x: 48.584576, y: 8.186118))
52 | path.addCurve(to: CGPoint(x: 47.865833, y: 11.33462), controlPoint1: CGPoint(x: 49.289585, y: 9.596135), controlPoint2: CGPoint(x: 48.878941, y: 10.828066))
53 | path.addLine(to: CGPoint(x: 47.865833, y: 11.33462))
54 | path.addLine(to: CGPoint(x: 32.968113, y: 18.78348))
55 | path.addCurve(to: CGPoint(x: 32.050915, y: 19), controlPoint1: CGPoint(x: 32.683334, y: 18.925869), controlPoint2: CGPoint(x: 32.369312, y: 19))
56 | path.addCurve(to: CGPoint(x: 30, y: 16.949083), controlPoint1: CGPoint(x: 30.918226, y: 19), controlPoint2: CGPoint(x: 30, y: 18.081774))
57 | path.addLine(to: CGPoint(x: 30, y: 16.949083))
58 | path.addLine(to: CGPoint(x: 30, y: 2.051366))
59 | path.addCurve(to: CGPoint(x: 30.21652, y: 1.134168), controlPoint1: CGPoint(x: 30, y: 1.732971), controlPoint2: CGPoint(x: 30.074131, y: 1.418949))
60 | path.close()
61 | path.move(to: CGPoint(x: 24.5, y: 6))
62 | path.addCurve(to: CGPoint(x: 28, y: 9.5), controlPoint1: CGPoint(x: 26.432997, y: 6), controlPoint2: CGPoint(x: 28, y: 7.567003))
63 | path.addCurve(to: CGPoint(x: 24.5, y: 13), controlPoint1: CGPoint(x: 28, y: 11.432997), controlPoint2: CGPoint(x: 26.432997, y: 13))
64 | path.addCurve(to: CGPoint(x: 21, y: 9.5), controlPoint1: CGPoint(x: 22.567003, y: 13), controlPoint2: CGPoint(x: 21, y: 11.432997))
65 | path.addCurve(to: CGPoint(x: 24.5, y: 6), controlPoint1: CGPoint(x: 21, y: 7.567003), controlPoint2: CGPoint(x: 22.567003, y: 6))
66 | path.close()
67 |
68 | path.apply(.init(translationX: -24.5, y: 0))
69 |
70 | return path
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/Sources/SlidingRuler/PreferenceKeys.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PreferenceKeys.swift
3 | //
4 | // SlidingRuler
5 | //
6 | // MIT License
7 | //
8 | // Copyright (c) 2020 Pierre Tacchi
9 | //
10 | // Permission is hereby granted, free of charge, to any person obtaining a copy
11 | // of this software and associated documentation files (the "Software"), to deal
12 | // in the Software without restriction, including without limitation the rights
13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14 | // copies of the Software, and to permit persons to whom the Software is
15 | // furnished to do so, subject to the following conditions:
16 | //
17 | // The above copyright notice and this permission notice shall be included in all
18 | // copies or substantial portions of the Software.
19 | //
20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26 | // SOFTWARE.
27 | //
28 |
29 |
30 | import SwiftUI
31 |
32 | struct ControlWidthPreferenceKey: PreferenceKey {
33 | static var defaultValue: CGFloat?
34 | static func reduce(value: inout CGFloat?, nextValue: () -> CGFloat?) {
35 | if let newValue = nextValue() { value = newValue }
36 | }
37 | }
38 |
39 | struct MarkOffsetPreferenceKey: PreferenceKey {
40 | static var defaultValue: CGFloat = .zero
41 | static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
42 | value = nextValue()
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/Sources/SlidingRuler/Ruler/Ruler.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Ruler.swift
3 | //
4 | // SlidingRuler
5 | //
6 | // MIT License
7 | //
8 | // Copyright (c) 2020 Pierre Tacchi
9 | //
10 | // Permission is hereby granted, free of charge, to any person obtaining a copy
11 | // of this software and associated documentation files (the "Software"), to deal
12 | // in the Software without restriction, including without limitation the rights
13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14 | // copies of the Software, and to permit persons to whom the Software is
15 | // furnished to do so, subject to the following conditions:
16 | //
17 | // The above copyright notice and this permission notice shall be included in all
18 | // copies or substantial portions of the Software.
19 | //
20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26 | // SOFTWARE.
27 | //
28 |
29 |
30 | import SwiftUI
31 |
32 | struct Ruler: View, Equatable {
33 | @Environment(\.slidingRulerStyle) private var style
34 |
35 | let cells: [RulerCell]
36 | let step: CGFloat
37 | let markOffset: CGFloat
38 | let bounds: ClosedRange
39 | let formatter: NumberFormatter?
40 |
41 | var body: some View {
42 | HStack(spacing: 0) {
43 | ForEach(self.cells) { cell in
44 | self.style.makeCellBody(configuration: self.configuration(forCell: cell))
45 | }
46 | }
47 | .animation(nil)
48 | }
49 |
50 | private func configuration(forCell cell: RulerCell) -> SlidingRulerStyleConfiguation {
51 | return .init(mark: (cell.mark + markOffset) * step, bounds: bounds, step: step, formatter: formatter)
52 | }
53 |
54 | static func ==(lhs: Self, rhs: Self) -> Bool {
55 | lhs.step == rhs.step &&
56 | lhs.cells.count == rhs.cells.count &&
57 | (!StaticSlidingRulerStyleEnvironment.hasMarks || lhs.markOffset == rhs.markOffset)
58 | }
59 | }
60 |
61 | struct Ruler_Previews: PreviewProvider {
62 | static var previews: some View {
63 | Ruler(cells: [.init(CGFloat(0))],
64 | step: 1.0, markOffset: 0, bounds: -1...1, formatter: nil)
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/Sources/SlidingRuler/Ruler/RulerCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RulerCell.swift
3 | //
4 | // SlidingRuler
5 | //
6 | // MIT License
7 | //
8 | // Copyright (c) 2020 Pierre Tacchi
9 | //
10 | // Permission is hereby granted, free of charge, to any person obtaining a copy
11 | // of this software and associated documentation files (the "Software"), to deal
12 | // in the Software without restriction, including without limitation the rights
13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14 | // copies of the Software, and to permit persons to whom the Software is
15 | // furnished to do so, subject to the following conditions:
16 | //
17 | // The above copyright notice and this permission notice shall be included in all
18 | // copies or substantial portions of the Software.
19 | //
20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26 | // SOFTWARE.
27 | //
28 |
29 |
30 | import CoreGraphics
31 |
32 | class RulerCell: Identifiable {
33 | var id: CGFloat { mark }
34 | var mark: CGFloat
35 |
36 | init(_ mark: Int) { self.mark = .init(mark) }
37 | init(_ mark: CGFloat) { self.mark = mark }
38 | }
39 |
--------------------------------------------------------------------------------
/Sources/SlidingRuler/SlidingRuler.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SlidingRuler.swift
3 | //
4 | // SlidingRuler
5 | //
6 | // MIT License
7 | //
8 | // Copyright (c) 2020 Pierre Tacchi
9 | //
10 | // Permission is hereby granted, free of charge, to any person obtaining a copy
11 | // of this software and associated documentation files (the "Software"), to deal
12 | // in the Software without restriction, including without limitation the rights
13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14 | // copies of the Software, and to permit persons to whom the Software is
15 | // furnished to do so, subject to the following conditions:
16 | //
17 | // The above copyright notice and this permission notice shall be included in all
18 | // copies or substantial portions of the Software.
19 | //
20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26 | // SOFTWARE.
27 | //
28 |
29 |
30 | import SwiftUI
31 | import SmoothOperators
32 |
33 | @available(iOS 13.0, *)
34 | public struct SlidingRuler: View where V: BinaryFloatingPoint, V.Stride: BinaryFloatingPoint {
35 |
36 | @Environment(\.slidingRulerCellOverflow) private var cellOverflow
37 |
38 | @Environment(\.slidingRulerStyle) private var style
39 | @Environment(\.slidingRulerStyle.cellWidth) private var cellWidth
40 | @Environment(\.slidingRulerStyle.cursorAlignment) private var verticalCursorAlignment
41 | @Environment(\.slidingRulerStyle.fractions) private var fractions
42 | @Environment(\.slidingRulerStyle.hasHalf) private var hasHalf
43 |
44 | @Environment(\.layoutDirection) private var layoutDirection
45 |
46 | /// Bound value.
47 | @Binding private var controlValue: V
48 | /// Possible value range.
49 | private let bounds: ClosedRange
50 | /// Value stride.
51 | private let step: CGFloat
52 | /// When to snap.
53 | private let snap: Mark
54 | /// When to tick.
55 | private let tick: Mark
56 | /// Edit changed callback.
57 | private let editingChangedCallback: (Bool) -> ()
58 | /// Number formatter for ruler's marks.
59 | private let formatter: NumberFormatter?
60 |
61 | /// Width of the control, retrieved through preference key.
62 | @State private var controlWidth: CGFloat?
63 | /// Height of the ruller, retrieved through preference key.
64 | @State private var rulerHeight: CGFloat?
65 |
66 | /// Cells of the ruler.
67 | @State private var cells: [RulerCell] = [.init(CGFloat(0))]
68 |
69 | /// Control state.
70 | @State private var state: SlidingRulerState = .idle
71 | /// The reference offset set at the start of a drag session.
72 | @State private var referenceOffset: CGSize = .zero
73 | /// The virtual ruler's drag offset.
74 | @State private var dragOffset: CGSize = .zero
75 | /// Offset of the ruler's displayed marks.
76 | @State private var markOffset: CGFloat = .zero
77 |
78 | /// Non-bound value used for rubber release animation.
79 | @State private var animatedValue: CGFloat = .zero
80 | /// The last value the receiver did set. Used to define if the rendered value was set by the receiver or from another component.
81 | @State private var lastValueSet: CGFloat = .zero
82 |
83 | /// VSynch timer that drives animations.
84 | @State private var animationTimer: VSynchedTimer? = nil
85 |
86 | private var value: CGFloat {
87 | get { CGFloat(controlValue) ?? 0 }
88 | nonmutating set { controlValue = V(newValue) }
89 | }
90 |
91 | /// Allowed drag offset range.
92 | private var dragBounds: ClosedRange {
93 | let lower = bounds.upperBound.isInfinite ? -CGFloat.infinity : -bounds.upperBound * cellWidth / step
94 | let upper = bounds.lowerBound.isInfinite ? CGFloat.infinity : -bounds.lowerBound * cellWidth / step
95 | return .init(uncheckedBounds: (lower, upper))
96 | }
97 |
98 | /// Over-ranged drag rubber should be released.
99 | private var isRubberBandNeedingRelease: Bool { !dragBounds.contains(dragOffset.width) }
100 | /// Amount of units the ruler can translate in both direction before needing to refresh the cells and reset offset.
101 | private var cellWidthOverflow: CGFloat { cellWidth * CGFloat(cellOverflow) }
102 | /// Current value clamped to the receiver's value bounds.
103 | private var clampedValue: CGFloat { value.clamped(to: bounds) }
104 |
105 | /// Ruler offset used to render the control depending on the state.
106 | private var effectiveOffset: CGSize {
107 | switch state {
108 | case .idle:
109 | return self.offset(fromValue: clampedValue ?? 0)
110 | case .animating:
111 | return self.offset(fromValue: animatedValue ?? 0)
112 | default:
113 | return dragOffset
114 | }
115 | }
116 |
117 | /// Creates a SlidingRuler
118 | /// - Parameters:
119 | /// - value: A binding connected to the control value.
120 | /// - bounds: A closed range of possible values. Defaults to `-V.infinity...V.infinity`.
121 | /// - step: The stride of the SlidingRuler. Defaults to `1`.
122 | /// - snap: The ruler's marks stickyness. Defaults to `.none`
123 | /// - tick: The graduation type that produce an haptic feedback when reached. Defaults to `.none`
124 | /// - onEditingChanged: A closure executed when a drag session happens. It receives a boolean value set to `true` when the drag session starts and `false` when the value stops changing. Defaults to no action.
125 | /// - formatter: A `NumberFormatter` instance the ruler uses to format the ruler's marks. Defaults to `nil`.
126 | public init(value: Binding,
127 | in bounds: ClosedRange = -V.infinity...V.infinity,
128 | step: V.Stride = 1,
129 | snap: Mark = .none,
130 | tick: Mark = .none,
131 | onEditingChanged: @escaping (Bool) -> () = { _ in },
132 | formatter: NumberFormatter? = nil) {
133 | self._controlValue = value
134 | self.bounds = .init(uncheckedBounds: (CGFloat(bounds.lowerBound), CGFloat(bounds.upperBound)))
135 | self.step = CGFloat(step)
136 | self.snap = snap
137 | self.tick = tick
138 | self.editingChangedCallback = onEditingChanged
139 | self.formatter = formatter
140 | }
141 |
142 | // MARK: Rendering
143 |
144 | public var body: some View {
145 | let renderedValue: CGFloat, renderedOffset: CGSize
146 |
147 | (renderedValue, renderedOffset) = renderingValues()
148 |
149 | return FlexibleWidthContainer {
150 | ZStack(alignment: .init(horizontal: .center, vertical: self.verticalCursorAlignment)) {
151 | Ruler(cells: self.cells, step: self.step, markOffset: self.markOffset, bounds: self.bounds, formatter: self.formatter)
152 | .equatable()
153 | .animation(nil)
154 | .modifier(InfiniteOffsetEffect(offset: renderedOffset, maxOffset: self.cellWidthOverflow))
155 | self.style.makeCursorBody()
156 | }
157 | }
158 | .modifier(InfiniteMarkOffsetModifier(renderedValue, step: step))
159 | .propagateWidth(ControlWidthPreferenceKey.self)
160 | .onPreferenceChange(MarkOffsetPreferenceKey.self, storeValueIn: $markOffset)
161 | .onPreferenceChange(ControlWidthPreferenceKey.self, storeValueIn: $controlWidth) {
162 | self.updateCellsIfNeeded()
163 | }
164 | .transaction {
165 | if $0.animation != nil { $0.animation = .easeIn(duration: 0.1) }
166 | }
167 | .onHorizontalDragGesture(initialTouch: firstTouchHappened,
168 | prematureEnd: panGestureEndedPrematurely,
169 | perform: horizontalDragAction(withValue:))
170 | }
171 |
172 | private func renderingValues() -> (CGFloat, CGSize) {
173 | let value: CGFloat
174 | let offset: CGSize
175 |
176 | switch self.state {
177 | case .flicking, .springing:
178 | if self.value != self.lastValueSet {
179 | self.animationTimer?.cancel()
180 | NextLoop { self.state = .idle }
181 | value = clampedValue ?? 0
182 | offset = self.offset(fromValue: value)
183 | } else {
184 | fallthrough
185 | }
186 | case .dragging, .stoppedFlick, .stoppedSpring:
187 | offset = dragOffset
188 | value = self.value(fromOffset: offset)
189 | case .animating:
190 | if self.value != lastValueSet {
191 | NextLoop { self.state = .idle }
192 | fallthrough
193 | }
194 | value = animatedValue
195 | offset = self.offset(fromValue: value)
196 | case .idle:
197 | value = clampedValue ?? 0
198 | offset = self.offset(fromValue: value)
199 | }
200 |
201 | return (value, offset)
202 | }
203 | }
204 |
205 | // MARK: Drag Gesture Management
206 | extension SlidingRuler {
207 |
208 | /// Callback handling first touch event.
209 | private func firstTouchHappened() {
210 | switch state {
211 | case .flicking:
212 | cancelCurrentTimer()
213 | state = .stoppedFlick
214 | case .springing:
215 | cancelCurrentTimer()
216 | state = .stoppedSpring
217 | default: break
218 | }
219 | }
220 |
221 | /// Callback handling gesture premature ending.
222 | private func panGestureEndedPrematurely() {
223 | switch state {
224 | case .stoppedFlick:
225 | state = .idle
226 | snapIfNeeded()
227 | case .stoppedSpring:
228 | releaseRubberBand()
229 | default:
230 | break
231 | }
232 | }
233 |
234 | /// Composite callback passed to the horizontal drag gesture recognizer.
235 | private func horizontalDragAction(withValue value: HorizontalDragGestureValue) {
236 | switch value.state {
237 | case .began: horizontalDragBegan(value)
238 | case .changed: horizontalDragChanged(value)
239 | case .ended: horizontalDragEnded(value)
240 | default: return
241 | }
242 | }
243 |
244 | /// Callback handling horizontal drag gesture begining.
245 | private func horizontalDragBegan(_ value: HorizontalDragGestureValue) {
246 | editingChangedCallback(true)
247 | if state != .stoppedSpring {
248 | dragOffset = self.offset(fromValue: clampedValue ?? 0)
249 | }
250 | referenceOffset = dragOffset
251 | state = .dragging
252 | }
253 |
254 | /// Callback handling horizontal drag gesture updating.
255 | private func horizontalDragChanged(_ value: HorizontalDragGestureValue) {
256 | let newOffset = self.directionalOffset(value.translation.horizontal + referenceOffset)
257 | let newValue = self.value(fromOffset: newOffset)
258 |
259 | self.tickIfNeeded(dragOffset, newOffset)
260 |
261 | withoutAnimation {
262 | self.setValue(newValue)
263 | dragOffset = self.applyRubber(to: newOffset)
264 | }
265 | }
266 |
267 | /// Callback handling horizontal drag gesture ending.
268 | private func horizontalDragEnded(_ value: HorizontalDragGestureValue) {
269 | if isRubberBandNeedingRelease {
270 | self.releaseRubberBand()
271 | self.endDragSession()
272 | } else if abs(value.velocity) > 90 {
273 | self.applyInertia(initialVelocity: value.velocity)
274 | } else {
275 | state = .idle
276 | self.endDragSession()
277 | self.snapIfNeeded()
278 | }
279 | }
280 |
281 | /// Drag session clean-up.
282 | private func endDragSession() {
283 | referenceOffset = .zero
284 | self.editingChangedCallback(false)
285 | }
286 | }
287 |
288 | // MARK: Value Management
289 | extension SlidingRuler {
290 |
291 | /// Compute the value from the given ruler's offset.
292 | private func value(fromOffset offset: CGSize) -> CGFloat {
293 | self.directionalValue(-CGFloat(offset.width / cellWidth) * step)
294 | }
295 |
296 | /// Compute the ruler's offset from the given value.
297 | private func offset(fromValue value: CGFloat) -> CGSize {
298 | let width = -value * cellWidth / step
299 | return self.directionalOffset(.init(horizontal: width))
300 | }
301 |
302 | /// Sets the value.
303 | private func setValue(_ newValue: CGFloat) {
304 | let clampedValue = newValue.clamped(to: bounds)
305 |
306 | if clampedValue.isBound(of: bounds) && !value.isBound(of: self.bounds) {
307 | self.boundaryMet()
308 | }
309 |
310 | if lastValueSet != clampedValue { lastValueSet = clampedValue }
311 | if value != clampedValue { value = clampedValue }
312 | }
313 |
314 | /// Snaps the value to the nearest mark based on the `snap` property.
315 | private func snapIfNeeded() {
316 | let nearest = self.nearestSnapValue(self.value)
317 | guard nearest != value else { return }
318 |
319 | let delta = abs(nearest - value)
320 | let fractionalValue = step / CGFloat(fractions)
321 |
322 | guard delta < fractionalValue else { return }
323 |
324 | let animThreshold = step / 200
325 | let animation: Animation? = delta > animThreshold ? .easeOut(duration: 0.1) : nil
326 |
327 | dragOffset = offset(fromValue: nearest)
328 | withAnimation(animation) { self.value = nearest }
329 | }
330 |
331 | /// Returns the nearest value to snap on based on the `snap` property.
332 | private func nearestSnapValue(_ value: CGFloat) -> CGFloat {
333 | guard snap != .none else { return value }
334 |
335 | let t: CGFloat
336 |
337 | switch snap {
338 | case .unit: t = step
339 | case .half: t = step / 2
340 | case .fraction: t = step / CGFloat(fractions)
341 | default: fatalError()
342 | }
343 |
344 | let lower = (value / t).rounded(.down) * t
345 | let upper = (value / t).rounded(.up) * t
346 | let deltaDown = abs(value - lower).approximated()
347 | let deltaUp = abs(value - upper).approximated()
348 |
349 | return deltaDown < deltaUp ? lower : upper
350 | }
351 |
352 | /// Transforms any numerical value based the layout direction. /!\ not properly tested.
353 | func directionalValue(_ value: T) -> T {
354 | value * (layoutDirection == .rightToLeft ? -1 : 1)
355 | }
356 |
357 | /// Transforms an offsetr based on the layout direction. /!\ not properly tested.
358 | func directionalOffset(_ offset: CGSize) -> CGSize {
359 | let width = self.directionalValue(offset.width)
360 | return .init(width: width, height: offset.height)
361 | }
362 | }
363 |
364 | // MARK: Control Update
365 | extension SlidingRuler {
366 |
367 | /// Adjusts the number of cells as the control size changes.
368 | private func updateCellsIfNeeded() {
369 | guard let controlWidth = controlWidth else { return }
370 | let count = (Int(ceil(controlWidth / cellWidth)) + cellOverflow * 2).nextOdd()
371 | if count != cells.count { self.populateCells(count: count) }
372 | }
373 |
374 | /// Creates `count` cells for the ruler.
375 | private func populateCells(count: Int) {
376 | let boundary = count.previousEven() / 2
377 | cells = (-boundary...boundary).map { .init($0) }
378 | }
379 | }
380 |
381 | extension UIScrollView.DecelerationRate {
382 | static var ruler: Self { Self.init(rawValue: 0.9972) }
383 | }
384 |
385 | // MARK: Mechanic Simulation
386 | extension SlidingRuler {
387 |
388 | private func applyInertia(initialVelocity: CGFloat) {
389 | func shiftOffset(by distance: CGSize) {
390 | let newOffset = directionalOffset(self.referenceOffset + distance)
391 | let newValue = self.value(fromOffset: newOffset)
392 |
393 | self.tickIfNeeded(self.dragOffset, newOffset)
394 |
395 | withoutAnimation {
396 | self.setValue(newValue)
397 | self.dragOffset = newOffset
398 | }
399 | }
400 |
401 | referenceOffset = dragOffset
402 |
403 | let rate = UIScrollView.DecelerationRate.ruler
404 | let totalDistance = Mechanic.Inertia.totalDistance(forVelocity: initialVelocity, decelerationRate: rate)
405 | let finalOffset = self.referenceOffset + .init(horizontal: totalDistance)
406 |
407 | state = .flicking
408 |
409 | if dragBounds.contains(finalOffset.width) {
410 | let duration = Mechanic.Inertia.duration(forVelocity: initialVelocity, decelerationRate: rate)
411 |
412 | animationTimer = .init(duration: duration, animations: { (progress, interval) in
413 | let distance = CGSize(horizontal: Mechanic.Inertia.distance(atTime: progress, v0: initialVelocity, decelerationRate: rate))
414 | shiftOffset(by: distance)
415 | }, completion: { (completed) in
416 | if completed {
417 | self.state = .idle
418 | shiftOffset(by: .init(horizontal: totalDistance))
419 | self.snapIfNeeded()
420 | self.endDragSession()
421 | } else {
422 | NextLoop { self.endDragSession() }
423 | }
424 | })
425 | } else {
426 | let allowedDistance = finalOffset.width.clamped(to: dragBounds) - self.referenceOffset.width
427 | let duration = Mechanic.Inertia.time(toReachDistance: allowedDistance, forVelocity: initialVelocity, decelerationRate: rate)
428 | animationTimer = .init(duration: duration, animations: { (progress, interval) in
429 | let distance = CGSize(horizontal: Mechanic.Inertia.distance(atTime: progress, v0: initialVelocity, decelerationRate: rate))
430 | shiftOffset(by: distance)
431 | }, completion: { (completed) in
432 | if completed {
433 | shiftOffset(by: .init(horizontal: allowedDistance))
434 | let remainingVelocity = Mechanic.Inertia.velocity(atTime: duration, v0: initialVelocity, decelerationRate: rate)
435 | self.applyInertialRubber(remainingVelocity: remainingVelocity)
436 | self.endDragSession()
437 | } else {
438 | NextLoop { self.endDragSession() }
439 | }
440 | })
441 | }
442 | }
443 |
444 | private func applyInertialRubber(remainingVelocity: CGFloat) {
445 | let duration = Mechanic.Spring.duration(forVelocity: abs(remainingVelocity), displacement: 0)
446 | let targetOffset = dragOffset.width.nearestBound(of: dragBounds)
447 |
448 | state = .springing
449 |
450 | animationTimer = .init(duration: duration, animations: { (progress, interval) in
451 | let delta = Mechanic.Spring.value(atTime: progress, v0: remainingVelocity, displacement: 0)
452 | self.dragOffset = .init(horizontal: targetOffset + delta)
453 | }, completion: { (completed) in
454 | if completed {
455 | self.dragOffset = .init(horizontal: targetOffset)
456 | self.state = .idle
457 | }
458 | })
459 | }
460 |
461 | /// Applies rubber effect to an off-range offset.
462 | private func applyRubber(to offset: CGSize) -> CGSize {
463 | let dragBounds = self.dragBounds
464 | guard !dragBounds.contains(offset.width) else { return offset }
465 |
466 | let tx = offset.width
467 | let limit = tx.clamped(to: dragBounds)
468 | let delta = abs(tx - limit)
469 | let factor: CGFloat = tx - limit < 0 ? -1 : 1
470 | let d = controlWidth ?? 0
471 | let c = CGFloat(0.55)
472 | let rubberDelta = (1 - (1 / ((c * delta / d) + 1))) * d * factor
473 | let rubberTx = limit + rubberDelta
474 |
475 | return .init(horizontal: rubberTx)
476 | }
477 |
478 | /// Animates an off-range offset back in place
479 | private func releaseRubberBand() {
480 | let targetOffset = dragOffset.width.clamped(to: dragBounds)
481 | let delta = dragOffset.width - targetOffset
482 | let duration = Mechanic.Spring.duration(forVelocity: 0, displacement: abs(delta))
483 |
484 | state = .springing
485 |
486 | animationTimer = .init(duration: duration, animations: { (progress, interval) in
487 | let newDelta = Mechanic.Spring.value(atTime: progress, v0: 0, displacement: delta)
488 | self.dragOffset = .init(horizontal: targetOffset + newDelta)
489 | }, completion: { (completed) in
490 | if completed {
491 | self.dragOffset = .init(horizontal: targetOffset)
492 | self.state = .idle
493 | }
494 | })
495 | }
496 |
497 | /// Stops the current animation and cleans the timer.
498 | private func cancelCurrentTimer() {
499 | animationTimer?.cancel()
500 | animationTimer = nil
501 | }
502 |
503 | private func cleanTimer() {
504 | animationTimer = nil
505 | }
506 | }
507 |
508 |
509 |
510 | // MARK: Tick Management
511 | extension SlidingRuler {
512 | private func boundaryMet() {
513 | let fg = UIImpactFeedbackGenerator(style: .rigid)
514 | fg.impactOccurred(intensity: 0.667)
515 | }
516 |
517 | private func tickIfNeeded(_ offset0: CGSize, _ offset1: CGSize) {
518 | let width0 = offset0.width, width1 = offset1.width
519 |
520 | let dragBounds = self.dragBounds
521 | guard dragBounds.contains(width0), dragBounds.contains(width1),
522 | !width0.isBound(of: dragBounds), !width1.isBound(of: dragBounds) else { return }
523 |
524 | let t: CGFloat
525 | switch tick {
526 | case .unit: t = cellWidth
527 | case .half: t = hasHalf ? cellWidth / 2 : cellWidth
528 | case .fraction: t = cellWidth / CGFloat(fractions)
529 | case .none: return
530 | }
531 |
532 | if width1 == 0 ||
533 | (width0 < 0) != (width1 < 0) ||
534 | Int((width0 / t).approximated()) != Int((width1 / t).approximated()) {
535 | valueTick()
536 | }
537 | }
538 |
539 | private func valueTick() {
540 | let fg = UIImpactFeedbackGenerator(style: .light)
541 | fg.impactOccurred(intensity: 0.5)
542 | }
543 | }
544 |
545 |
--------------------------------------------------------------------------------
/Sources/SlidingRuler/Styling/AnyFractionableView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AnyFractionableView.swift
3 | //
4 | // SlidingRuler
5 | //
6 | // MIT License
7 | //
8 | // Copyright (c) 2020 Pierre Tacchi
9 | //
10 | // Permission is hereby granted, free of charge, to any person obtaining a copy
11 | // of this software and associated documentation files (the "Software"), to deal
12 | // in the Software without restriction, including without limitation the rights
13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14 | // copies of the Software, and to permit persons to whom the Software is
15 | // furnished to do so, subject to the following conditions:
16 | //
17 | // The above copyright notice and this permission notice shall be included in all
18 | // copies or substantial portions of the Software.
19 | //
20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26 | // SOFTWARE.
27 | //
28 |
29 |
30 | import SwiftUI
31 |
32 | struct AnyFractionableView: FractionableView {
33 | static var fractions: Int { 0 }
34 | private let view: AnyView
35 | var body: some View { view }
36 | init(_ view: V) { self.view = AnyView(view) }
37 | }
38 |
39 |
--------------------------------------------------------------------------------
/Sources/SlidingRuler/Styling/AnySlidingRulerStyle.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AnySlidingRulerStyle.swift
3 | //
4 | // SlidingRuler
5 | //
6 | // MIT License
7 | //
8 | // Copyright (c) 2020 Pierre Tacchi
9 | //
10 | // Permission is hereby granted, free of charge, to any person obtaining a copy
11 | // of this software and associated documentation files (the "Software"), to deal
12 | // in the Software without restriction, including without limitation the rights
13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14 | // copies of the Software, and to permit persons to whom the Software is
15 | // furnished to do so, subject to the following conditions:
16 | //
17 | // The above copyright notice and this permission notice shall be included in all
18 | // copies or substantial portions of the Software.
19 | //
20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26 | // SOFTWARE.
27 | //
28 |
29 |
30 | import SwiftUI
31 |
32 | struct AnySlidingRulerStyle: SlidingRulerStyle {
33 | private let cellProvider: (SlidingRulerStyleConfiguation) -> AnyFractionableView
34 | private let cursorProvider: () -> AnyView
35 |
36 | let fractions: Int
37 | let cellWidth: CGFloat
38 | let cursorAlignment: VerticalAlignment
39 | let hasMarks: Bool
40 |
41 | init (style: T) {
42 | self.cellProvider = { (configuration: SlidingRulerStyleConfiguation) -> AnyFractionableView in
43 | AnyFractionableView(style.makeCellBody(configuration: configuration))
44 | }
45 | self.cursorProvider = {
46 | AnyView(style.makeCursorBody())
47 | }
48 | self.fractions = style.fractions
49 | self.cellWidth = style.cellWidth
50 | self.cursorAlignment = style.cursorAlignment
51 | self.hasMarks = style.hasMarks
52 | }
53 |
54 | func makeCellBody(configuration: SlidingRulerStyleConfiguation) -> some FractionableView {
55 | cellProvider(configuration)
56 | }
57 |
58 | func makeCursorBody() -> some View {
59 | cursorProvider()
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/Sources/SlidingRuler/Styling/CenteredStyle/BlankCenteredStyle.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BlankCenteredStyle.swift
3 | //
4 | // SlidingRuler
5 | //
6 | // MIT License
7 | //
8 | // Copyright (c) 2020 Pierre Tacchi
9 | //
10 | // Permission is hereby granted, free of charge, to any person obtaining a copy
11 | // of this software and associated documentation files (the "Software"), to deal
12 | // in the Software without restriction, including without limitation the rights
13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14 | // copies of the Software, and to permit persons to whom the Software is
15 | // furnished to do so, subject to the following conditions:
16 | //
17 | // The above copyright notice and this permission notice shall be included in all
18 | // copies or substantial portions of the Software.
19 | //
20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26 | // SOFTWARE.
27 | //
28 |
29 |
30 | import SwiftUI
31 |
32 | public struct BlankCenteredSlidingRulerStyle: SlidingRulerStyle {
33 | public let cursorAlignment: VerticalAlignment = .top
34 |
35 | public func makeCellBody(configuration: SlidingRulerStyleConfiguation) -> some FractionableView {
36 | BlankCenteredCellBody(mark: configuration.mark,
37 | bounds: configuration.bounds,
38 | step: configuration.step,
39 | cellWidth: cellWidth)
40 | }
41 |
42 | public func makeCursorBody() -> some View {
43 | NativeCursorBody()
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/Sources/SlidingRuler/Styling/CenteredStyle/CenteredCellBody.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CenteredCellBody.swift
3 | //
4 | // SlidingRuler
5 | //
6 | // MIT License
7 | //
8 | // Copyright (c) 2020 Pierre Tacchi
9 | //
10 | // Permission is hereby granted, free of charge, to any person obtaining a copy
11 | // of this software and associated documentation files (the "Software"), to deal
12 | // in the Software without restriction, including without limitation the rights
13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14 | // copies of the Software, and to permit persons to whom the Software is
15 | // furnished to do so, subject to the following conditions:
16 | //
17 | // The above copyright notice and this permission notice shall be included in all
18 | // copies or substantial portions of the Software.
19 | //
20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26 | // SOFTWARE.
27 | //
28 |
29 |
30 | import SwiftUI
31 |
32 | struct BlankCenteredCellBody: NativeRulerCellView {
33 | var mark: CGFloat
34 | var bounds: ClosedRange
35 | var step: CGFloat
36 | var cellWidth: CGFloat
37 |
38 | var scale: some ScaleView { CenteredScaleView(width: cellWidth) }
39 | }
40 |
41 | struct CenteredCellBody: NativeMarkedRulerCellView {
42 | var mark: CGFloat
43 | var bounds: ClosedRange
44 | var step: CGFloat
45 | var cellWidth: CGFloat
46 | var numberFormatter: NumberFormatter?
47 |
48 | var cell: some RulerCellView { BlankCenteredCellBody(mark: mark, bounds: bounds, step: step, cellWidth: cellWidth) }
49 | }
50 |
--------------------------------------------------------------------------------
/Sources/SlidingRuler/Styling/CenteredStyle/CenteredScaleView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BlankCellView.swift
3 | //
4 | // SlidingRuler
5 | //
6 | // MIT License
7 | //
8 | // Copyright (c) 2020 Pierre Tacchi
9 | //
10 | // Permission is hereby granted, free of charge, to any person obtaining a copy
11 | // of this software and associated documentation files (the "Software"), to deal
12 | // in the Software without restriction, including without limitation the rights
13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14 | // copies of the Software, and to permit persons to whom the Software is
15 | // furnished to do so, subject to the following conditions:
16 | //
17 | // The above copyright notice and this permission notice shall be included in all
18 | // copies or substantial portions of the Software.
19 | //
20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26 | // SOFTWARE.
27 | //
28 |
29 |
30 | import SwiftUI
31 |
32 | struct CenteredScaleView: ScaleView {
33 | struct ScaleShape: Shape {
34 | fileprivate var unitMarkSize: CGSize { .init(width: 3.0, height: 27.0)}
35 | fileprivate var halfMarkSize: CGSize { .init(width: UIScreen.main.scale == 3 ? 1.8 : 2.0, height: 19.0) }
36 | fileprivate var fractionMarkSize: CGSize { .init(width: 1.0, height: 11.0)}
37 |
38 | func path(in rect: CGRect) -> Path {
39 | let centerX = rect.center.x
40 | let centerY = rect.center.y
41 | var p = Path()
42 |
43 | p.addRoundedRect(in: unitRect(x: centerX, y: centerY), cornerSize: .init(square: unitMarkSize.width/2))
44 | p.addRoundedRect(in: halfRect(x: 0, y: centerY), cornerSize: .init(square: halfMarkSize.width/2))
45 | p.addRoundedRect(in: halfRect(x: rect.maxX, y: centerY), cornerSize: .init(square: halfMarkSize.width/2))
46 |
47 | let tenth = rect.width / 10
48 | for i in 1...4 {
49 | p.addRoundedRect(in: tenthRect(x: centerX + CGFloat(i) * tenth, y: centerY), cornerSize: .init(square: fractionMarkSize.width/2))
50 | p.addRoundedRect(in: tenthRect(x: centerX - CGFloat(i) * tenth, y: centerY), cornerSize: .init(square: fractionMarkSize.width/2))
51 | }
52 |
53 | return p
54 | }
55 |
56 | private func unitRect(x: CGFloat, y: CGFloat) -> CGRect { .init(center: .init(x: x, y: y), size: unitMarkSize) }
57 | private func halfRect(x: CGFloat, y: CGFloat) -> CGRect { .init(center: .init(x: x, y: y), size: halfMarkSize) }
58 | private func tenthRect(x: CGFloat, y: CGFloat) -> CGRect { .init(center: .init(x: x, y: y), size: fractionMarkSize) }
59 | }
60 |
61 | let width: CGFloat
62 | let height: CGFloat
63 | var shape: ScaleShape { .init() }
64 |
65 | var unitMarkWidth: CGFloat { shape.unitMarkSize.width }
66 | var halfMarkWidth: CGFloat { shape.halfMarkSize.width }
67 | var fractionMarkWidth: CGFloat { shape.fractionMarkSize.width }
68 |
69 | init(width: CGFloat, height: CGFloat = 30) {
70 | self.width = width
71 | self.height = height
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/Sources/SlidingRuler/Styling/CenteredStyle/CenteredStyle.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CenteredStyle.swift
3 | //
4 | // SlidingRulerTestingBoard
5 | //
6 | // MIT License
7 | //
8 | // Copyright (c) 2020 Pierre Tacchi
9 | //
10 | // Permission is hereby granted, free of charge, to any person obtaining a copy
11 | // of this software and associated documentation files (the "Software"), to deal
12 | // in the Software without restriction, including without limitation the rights
13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14 | // copies of the Software, and to permit persons to whom the Software is
15 | // furnished to do so, subject to the following conditions:
16 | //
17 | // The above copyright notice and this permission notice shall be included in all
18 | // copies or substantial portions of the Software.
19 | //
20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26 | // SOFTWARE.
27 | //
28 |
29 |
30 | import SwiftUI
31 |
32 | public struct CenteredSlindingRulerStyle: SlidingRulerStyle {
33 | public var cursorAlignment: VerticalAlignment = .top
34 |
35 | public func makeCellBody(configuration: SlidingRulerStyleConfiguation) -> some FractionableView {
36 | CenteredCellBody(mark: configuration.mark,
37 | bounds: configuration.bounds,
38 | step: configuration.step,
39 | cellWidth: cellWidth,
40 | numberFormatter: configuration.formatter)
41 | }
42 |
43 | public func makeCursorBody() -> some View {
44 | NativeCursorBody()
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/Sources/SlidingRuler/Styling/DefaultStyle/BlankStyle.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BlankStyle.swift
3 | //
4 | // SlidingRuler
5 | //
6 | // MIT License
7 | //
8 | // Copyright (c) 2020 Pierre Tacchi
9 | //
10 | // Permission is hereby granted, free of charge, to any person obtaining a copy
11 | // of this software and associated documentation files (the "Software"), to deal
12 | // in the Software without restriction, including without limitation the rights
13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14 | // copies of the Software, and to permit persons to whom the Software is
15 | // furnished to do so, subject to the following conditions:
16 | //
17 | // The above copyright notice and this permission notice shall be included in all
18 | // copies or substantial portions of the Software.
19 | //
20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26 | // SOFTWARE.
27 | //
28 |
29 |
30 | import SwiftUI
31 |
32 | public struct BlankSlidingRulerStyle: SlidingRulerStyle {
33 | public let cursorAlignment: VerticalAlignment = .top
34 |
35 | public func makeCellBody(configuration: SlidingRulerStyleConfiguation) -> some FractionableView {
36 | BlankCellBody(mark: configuration.mark,
37 | bounds: configuration.bounds,
38 | step: configuration.step,
39 | cellWidth: cellWidth)
40 | }
41 |
42 | public func makeCursorBody() -> some View {
43 | NativeCursorBody()
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/Sources/SlidingRuler/Styling/DefaultStyle/DefaultCellBody.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DefaultCellBody.swift
3 | //
4 | // SlidingRuler
5 | //
6 | // MIT License
7 | //
8 | // Copyright (c) 2020 Pierre Tacchi
9 | //
10 | // Permission is hereby granted, free of charge, to any person obtaining a copy
11 | // of this software and associated documentation files (the "Software"), to deal
12 | // in the Software without restriction, including without limitation the rights
13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14 | // copies of the Software, and to permit persons to whom the Software is
15 | // furnished to do so, subject to the following conditions:
16 | //
17 | // The above copyright notice and this permission notice shall be included in all
18 | // copies or substantial portions of the Software.
19 | //
20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26 | // SOFTWARE.
27 | //
28 |
29 |
30 | import SwiftUI
31 |
32 | struct BlankCellBody: NativeRulerCellView {
33 | var mark: CGFloat
34 | var bounds: ClosedRange
35 | var step: CGFloat
36 | var cellWidth: CGFloat
37 |
38 | var scale: some ScaleView { DefaultScaleView(width: cellWidth) }
39 | }
40 |
41 | struct DefaultCellBody: NativeMarkedRulerCellView {
42 | var mark: CGFloat
43 | var bounds: ClosedRange
44 | var step: CGFloat
45 | var cellWidth: CGFloat
46 | var numberFormatter: NumberFormatter?
47 |
48 | var cell: some RulerCellView { BlankCellBody(mark: mark, bounds: bounds, step: step, cellWidth: cellWidth) }
49 | }
50 |
51 |
--------------------------------------------------------------------------------
/Sources/SlidingRuler/Styling/DefaultStyle/DefaultScaleView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ScaleView.swift
3 | //
4 | // SlidingRuler
5 | //
6 | // MIT License
7 | //
8 | // Copyright (c) 2020 Pierre Tacchi
9 | //
10 | // Permission is hereby granted, free of charge, to any person obtaining a copy
11 | // of this software and associated documentation files (the "Software"), to deal
12 | // in the Software without restriction, including without limitation the rights
13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14 | // copies of the Software, and to permit persons to whom the Software is
15 | // furnished to do so, subject to the following conditions:
16 | //
17 | // The above copyright notice and this permission notice shall be included in all
18 | // copies or substantial portions of the Software.
19 | //
20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26 | // SOFTWARE.
27 | //
28 |
29 |
30 | import SwiftUI
31 |
32 | struct DefaultScaleView: ScaleView {
33 | struct ScaleShape: Shape {
34 | fileprivate var unitMarkSize: CGSize { .init(width: 3.0, height: 27.0)}
35 | fileprivate var halfMarkSize: CGSize { .init(width: UIScreen.main.scale == 3 ? 1.8 : 2.0, height: 19.0) }
36 | fileprivate var fractionMarkSize: CGSize { .init(width: 1.0, height: 11.0)}
37 |
38 | func path(in rect: CGRect) -> Path {
39 | let centerX = rect.center.x
40 | var p = Path()
41 |
42 | p.addRoundedRect(in: unitRect(x: centerX), cornerSize: .init(square: unitMarkSize.width/2))
43 | p.addRoundedRect(in: halfRect(x: 0), cornerSize: .init(square: halfMarkSize.width/2))
44 | p.addRoundedRect(in: halfRect(x: rect.maxX), cornerSize: .init(square: halfMarkSize.width/2))
45 |
46 | let tenth = rect.width / 10
47 | for i in 1...4 {
48 | p.addRoundedRect(in: tenthRect(x: centerX + CGFloat(i) * tenth), cornerSize: .init(square: fractionMarkSize.width/2))
49 | p.addRoundedRect(in: tenthRect(x: centerX - CGFloat(i) * tenth), cornerSize: .init(square: fractionMarkSize.width/2))
50 | }
51 |
52 | return p
53 | }
54 |
55 | private func unitRect(x: CGFloat) -> CGRect { rect(centerX: x, size: unitMarkSize) }
56 | private func halfRect(x: CGFloat) -> CGRect { rect(centerX: x, size: halfMarkSize) }
57 | private func tenthRect(x: CGFloat) -> CGRect { rect(centerX: x, size: fractionMarkSize) }
58 |
59 | private func rect(centerX x: CGFloat, size: CGSize) -> CGRect {
60 | CGRect(origin: .init(x: x - size.width / 2, y: 0), size: size)
61 | }
62 | }
63 |
64 | var shape: ScaleShape { .init() }
65 | let width: CGFloat
66 | let height: CGFloat
67 |
68 | var unitMarkWidth: CGFloat { shape.unitMarkSize.width }
69 | var halfMarkWidth: CGFloat { shape.halfMarkSize.width }
70 | var fractionMarkWidth: CGFloat { shape.fractionMarkSize.width }
71 |
72 | init(width: CGFloat, height: CGFloat = 30) {
73 | self.width = width
74 | self.height = height
75 | }
76 | }
77 |
78 | struct ScaleView_Previews: PreviewProvider {
79 | static var previews: some View {
80 | Group {
81 | DefaultScaleView(width: 120)
82 | }
83 | .previewLayout(.sizeThatFits)
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/Sources/SlidingRuler/Styling/DefaultStyle/DefaultStyle.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DefaultSlidingRulerStyle.swift
3 | //
4 | // SlidingRuler
5 | //
6 | // MIT License
7 | //
8 | // Copyright (c) 2020 Pierre Tacchi
9 | //
10 | // Permission is hereby granted, free of charge, to any person obtaining a copy
11 | // of this software and associated documentation files (the "Software"), to deal
12 | // in the Software without restriction, including without limitation the rights
13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14 | // copies of the Software, and to permit persons to whom the Software is
15 | // furnished to do so, subject to the following conditions:
16 | //
17 | // The above copyright notice and this permission notice shall be included in all
18 | // copies or substantial portions of the Software.
19 | //
20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26 | // SOFTWARE.
27 | //
28 |
29 |
30 | import SwiftUI
31 |
32 | public struct PrimarySlidingRulerStyle: SlidingRulerStyle {
33 | public let cursorAlignment: VerticalAlignment = .top
34 |
35 | public func makeCellBody(configuration: SlidingRulerStyleConfiguation) -> some FractionableView {
36 | DefaultCellBody(mark: configuration.mark,
37 | bounds: configuration.bounds,
38 | step: configuration.step,
39 | cellWidth: cellWidth,
40 | numberFormatter: configuration.formatter)
41 | }
42 |
43 | public func makeCursorBody() -> some View {
44 | NativeCursorBody()
45 | }
46 | }
47 |
48 | struct DefaultStyle_Previews: PreviewProvider {
49 | struct CellTrio: View {
50 | let range: ClosedRange
51 | let width: CGFloat
52 |
53 | var body: some View {
54 | HStack(spacing: 0) {
55 | BlankCellBody(mark: -1, bounds: range, step: 1, cellWidth: width).clipped()
56 | BlankCellBody(mark: 0, bounds: range, step: 1, cellWidth: width).clipped()
57 | BlankCellBody(mark: 1, bounds: range, step: 1, cellWidth: width).clipped()
58 | }
59 | }
60 | }
61 |
62 | static var previews: some View {
63 | CellTrio(range: -0.4...0.9, width: 120)
64 | .previewLayout(.sizeThatFits)
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/Sources/SlidingRuler/Styling/NativeCursorBody.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DefaultCursorBody.swift
3 | //
4 | // SlidingRuler
5 | //
6 | // MIT License
7 | //
8 | // Copyright (c) 2020 Pierre Tacchi
9 | //
10 | // Permission is hereby granted, free of charge, to any person obtaining a copy
11 | // of this software and associated documentation files (the "Software"), to deal
12 | // in the Software without restriction, including without limitation the rights
13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14 | // copies of the Software, and to permit persons to whom the Software is
15 | // furnished to do so, subject to the following conditions:
16 | //
17 | // The above copyright notice and this permission notice shall be included in all
18 | // copies or substantial portions of the Software.
19 | //
20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26 | // SOFTWARE.
27 | //
28 |
29 | import SwiftUI
30 |
31 | public struct NativeCursorBody: View {
32 | public var body: some View {
33 | Capsule()
34 | .foregroundColor(.red)
35 | .frame(width: UIScreen.main.scale == 3 ? 1.8 : 2, height: 30)
36 | }
37 | }
38 |
39 | struct CursorBody_Previews: PreviewProvider {
40 | static var previews: some View {
41 | NativeCursorBody()
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/Sources/SlidingRuler/Styling/Protocols/FractionableView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Fractionable.swift
3 | //
4 | // SlidingRuler
5 | //
6 | // MIT License
7 | //
8 | // Copyright (c) 2020 Pierre Tacchi
9 | //
10 | // Permission is hereby granted, free of charge, to any person obtaining a copy
11 | // of this software and associated documentation files (the "Software"), to deal
12 | // in the Software without restriction, including without limitation the rights
13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14 | // copies of the Software, and to permit persons to whom the Software is
15 | // furnished to do so, subject to the following conditions:
16 | //
17 | // The above copyright notice and this permission notice shall be included in all
18 | // copies or substantial portions of the Software.
19 | //
20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26 | // SOFTWARE.
27 | //
28 |
29 |
30 | import SwiftUI
31 |
32 | public protocol FractionableView: View {
33 | static var fractions: Int { get }
34 | static var hasHalf: Bool { get }
35 | }
36 |
37 | extension FractionableView {
38 | static var fractions: Int { 10 }
39 | static var hasHalf: Bool { fractions.isEven }
40 | }
41 |
--------------------------------------------------------------------------------
/Sources/SlidingRuler/Styling/Protocols/MarkedRulerCellView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MarkedCellBody.swift
3 | //
4 | // SlidingRuler
5 | //
6 | // MIT License
7 | //
8 | // Copyright (c) 2020 Pierre Tacchi
9 | //
10 | // Permission is hereby granted, free of charge, to any person obtaining a copy
11 | // of this software and associated documentation files (the "Software"), to deal
12 | // in the Software without restriction, including without limitation the rights
13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14 | // copies of the Software, and to permit persons to whom the Software is
15 | // furnished to do so, subject to the following conditions:
16 | //
17 | // The above copyright notice and this permission notice shall be included in all
18 | // copies or substantial portions of the Software.
19 | //
20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26 | // SOFTWARE.
27 | //
28 |
29 |
30 | import SwiftUI
31 |
32 | public protocol MarkedRulerCellView: FractionableView {
33 | associatedtype CellView: RulerCellView
34 |
35 | var mark: CGFloat { get }
36 | var bounds: ClosedRange { get }
37 | var step: CGFloat { get }
38 | var cellWidth: CGFloat { get }
39 |
40 | var numberFormatter: NumberFormatter? { get }
41 | var markColor: Color { get }
42 | var cell: CellView { get }
43 | }
44 |
45 | extension MarkedRulerCellView {
46 | static var fractions: Int { CellView.fractions }
47 | }
48 |
--------------------------------------------------------------------------------
/Sources/SlidingRuler/Styling/Protocols/NativeMarkedRulerCellView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NativeMarkedCellBody.swift
3 | //
4 | // SlidingRuler
5 | //
6 | // MIT License
7 | //
8 | // Copyright (c) 2020 Pierre Tacchi
9 | //
10 | // Permission is hereby granted, free of charge, to any person obtaining a copy
11 | // of this software and associated documentation files (the "Software"), to deal
12 | // in the Software without restriction, including without limitation the rights
13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14 | // copies of the Software, and to permit persons to whom the Software is
15 | // furnished to do so, subject to the following conditions:
16 | //
17 | // The above copyright notice and this permission notice shall be included in all
18 | // copies or substantial portions of the Software.
19 | //
20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26 | // SOFTWARE.
27 | //
28 |
29 |
30 | import SwiftUI
31 |
32 | protocol NativeMarkedRulerCellView: MarkedRulerCellView { }
33 | extension NativeMarkedRulerCellView {
34 | var markColor: Color {
35 | bounds.contains(mark) ? .init(.label) : .init(.tertiaryLabel)
36 | }
37 | var displayMark: String { numberFormatter?.string(for: mark) ?? "\(mark.approximated())" }
38 |
39 | var body: some View {
40 | VStack {
41 | cell.equatable()
42 | Spacer()
43 | Text(verbatim: displayMark)
44 | .font(Font.footnote.monospacedDigit())
45 | .foregroundColor(markColor)
46 | .lineLimit(1)
47 | }
48 | .fixedSize()
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/Sources/SlidingRuler/Styling/Protocols/NativeRulerCellView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NativeCellBody.swift
3 | //
4 | // SlidingRuler
5 | //
6 | // MIT License
7 | //
8 | // Copyright (c) 2020 Pierre Tacchi
9 | //
10 | // Permission is hereby granted, free of charge, to any person obtaining a copy
11 | // of this software and associated documentation files (the "Software"), to deal
12 | // in the Software without restriction, including without limitation the rights
13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14 | // copies of the Software, and to permit persons to whom the Software is
15 | // furnished to do so, subject to the following conditions:
16 | //
17 | // The above copyright notice and this permission notice shall be included in all
18 | // copies or substantial portions of the Software.
19 | //
20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26 | // SOFTWARE.
27 | //
28 |
29 |
30 | import SwiftUI
31 | import SmoothOperators
32 |
33 | protocol NativeRulerCellView: RulerCellView { }
34 | extension NativeRulerCellView {
35 | var maskShape: some Shape {
36 | guard !isComplete else { return ScaleMask(originX: .zero, width: cellWidth) }
37 | guard cellBounds.overlaps(bounds) else { return ScaleMask.zero }
38 |
39 | let availableRange = bounds.clamped(to: cellBounds)
40 |
41 | let leadingOffset: CGFloat = adjustOffset(abs(CGFloat((availableRange.lowerBound - cellBounds.lowerBound) / step) * cellWidth).approximated())
42 | let trailingOffset: CGFloat = adjustOffset(abs(CGFloat((cellBounds.upperBound - availableRange.upperBound) / step) * cellWidth).approximated())
43 |
44 | let maskWidth = cellWidth - (leadingOffset + trailingOffset)
45 |
46 | return ScaleMask(originX: leadingOffset, width: maskWidth)
47 | }
48 |
49 | private var hasHalf: Bool { Scale.hasHalf }
50 | private var fractions: Int { Self.fractions }
51 | private var fractionWidth: CGFloat { cellWidth / CGFloat(fractions) }
52 |
53 | private func areaLimits(_ area: Int) -> (leading: CGFloat, trailing: CGFloat) {
54 | let leadingLimit: CGFloat
55 | let trailingLimit: CGFloat
56 |
57 | let leadingOffset = CGFloat(area) * fractionWidth
58 | let trailingOffset = CGFloat(area+) * fractionWidth
59 |
60 | if hasHalf {
61 | let leadingUnitArea = fractions / 2
62 | let trailingUnitArea = leadingUnitArea-
63 |
64 | if area == 0 { leadingLimit = scale.halfMarkWidth / 2 + scale.halfMarkOffset }
65 | else if area == leadingUnitArea { leadingLimit = leadingOffset + scale.unitMarkWidth / 2 + scale.unitMarkOffset }
66 | else { leadingLimit = leadingOffset + scale.fractionMarkWidth / 2 + scale.fractionMarkOffset }
67 |
68 | if area == fractions- { trailingLimit = trailingOffset - scale.unitMarkWidth / 2 + scale.unitMarkOffset }
69 | else if area == trailingUnitArea { trailingLimit = trailingOffset - scale.unitMarkWidth / 2 + scale.unitMarkOffset }
70 | else { trailingLimit = trailingOffset - scale.fractionMarkWidth / 2 + scale.fractionMarkOffset }
71 | } else {
72 | if area == 0 { leadingLimit = .nan }
73 | else if Scale.hasHalf && area == (fractions / 2) { leadingLimit = CGFloat(area) * fractionWidth + scale.halfMarkWidth / 2 }
74 | else { leadingLimit = CGFloat(area) * fractionWidth + fractionWidth / 2 }
75 |
76 | if area == fractions - 1 { trailingLimit = cellWidth - scale.unitMarkWidth / 2 }
77 | else if Scale.hasHalf && area == (fractions / 2 - 1) { trailingLimit = CGFloat(area + 1) * fractionWidth + scale.halfMarkWidth / 2 }
78 | else { trailingLimit = CGFloat(area + 1) * fractionWidth - fractionWidth / 2 }
79 | }
80 |
81 | return (leadingLimit, trailingLimit)
82 | }
83 |
84 | private func adjustOffset(_ offset: CGFloat) -> CGFloat {
85 | guard !offset.isZero else { return offset }
86 |
87 | let area = offset.truncatingRemainder(dividingBy: fractionWidth) > 0 ?
88 | Int(offset / fractionWidth) :
89 | Int(offset / fractionWidth)-
90 | let limits = areaLimits(area)
91 |
92 | if offset < limits.leading { return limits.leading }
93 | else if offset > limits.trailing { return limits.trailing }
94 | else { return offset }
95 | }
96 | }
97 |
98 | private struct ScaleMask: Shape {
99 | let originX: CGFloat
100 | let centerMaskWidth: CGFloat
101 |
102 | static var zero: Self { .init(originX: .zero, width: .zero) }
103 |
104 | init(originX: CGFloat, width: CGFloat) {
105 | self.originX = originX
106 | self.centerMaskWidth = width
107 | }
108 |
109 | func path(in rect: CGRect) -> Path {
110 | var p = Path()
111 | let centerRect = CGRect(origin: .init(x: originX, y: 0),
112 | size: .init(width: centerMaskWidth, height: rect.height))
113 | p.addRect(centerRect)
114 |
115 | return p
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/Sources/SlidingRuler/Styling/Protocols/RulerCellView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CellBody.swift
3 | //
4 | // SlidingRuler
5 | //
6 | // MIT License
7 | //
8 | // Copyright (c) 2020 Pierre Tacchi
9 | //
10 | // Permission is hereby granted, free of charge, to any person obtaining a copy
11 | // of this software and associated documentation files (the "Software"), to deal
12 | // in the Software without restriction, including without limitation the rights
13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14 | // copies of the Software, and to permit persons to whom the Software is
15 | // furnished to do so, subject to the following conditions:
16 | //
17 | // The above copyright notice and this permission notice shall be included in all
18 | // copies or substantial portions of the Software.
19 | //
20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26 | // SOFTWARE.
27 | //
28 |
29 |
30 | import SwiftUI
31 |
32 | public protocol RulerCellView: FractionableView, Equatable {
33 | associatedtype Scale: ScaleView
34 | associatedtype MaskShape: Shape
35 |
36 | var mark: CGFloat { get }
37 | var bounds: ClosedRange { get }
38 | var cellBounds: ClosedRange { get }
39 | var step: CGFloat { get }
40 | var cellWidth: CGFloat { get }
41 |
42 | var scale: Scale { get }
43 | var maskShape: MaskShape { get }
44 | }
45 |
46 | extension RulerCellView {
47 | static var fractions: Int { Scale.fractions }
48 |
49 | var cellBounds: ClosedRange {
50 | ClosedRange(uncheckedBounds: (mark - step / 2, mark + step / 2))
51 | }
52 |
53 | var isComplete: Bool { bounds.contains(cellBounds) }
54 |
55 | var body: some View {
56 | ZStack {
57 | scale
58 | .equatable()
59 | .foregroundColor(.init(.label))
60 | .clipShape(maskShape)
61 | scale
62 | .equatable()
63 | .foregroundColor(.init(.tertiaryLabel))
64 | }
65 | .frame(width: cellWidth)
66 | }
67 |
68 | static func ==(_ lhs: Self, _ rhs: Self) -> Bool {
69 | lhs.isComplete && rhs.isComplete
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/Sources/SlidingRuler/Styling/Protocols/ScaleView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ScaleView.swift
3 | //
4 | // SlidingRuler
5 | //
6 | // MIT License
7 | //
8 | // Copyright (c) 2020 Pierre Tacchi
9 | //
10 | // Permission is hereby granted, free of charge, to any person obtaining a copy
11 | // of this software and associated documentation files (the "Software"), to deal
12 | // in the Software without restriction, including without limitation the rights
13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14 | // copies of the Software, and to permit persons to whom the Software is
15 | // furnished to do so, subject to the following conditions:
16 | //
17 | // The above copyright notice and this permission notice shall be included in all
18 | // copies or substantial portions of the Software.
19 | //
20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26 | // SOFTWARE.
27 | //
28 |
29 |
30 | import SwiftUI
31 |
32 | public protocol ScaleView: FractionableView, Equatable {
33 | associatedtype ScaleShape: Shape
34 |
35 | var shape: ScaleShape { get }
36 | var width: CGFloat { get }
37 | var height: CGFloat { get }
38 |
39 | var unitMarkWidth: CGFloat { get }
40 | var halfMarkWidth: CGFloat { get }
41 | var fractionMarkWidth: CGFloat { get }
42 |
43 | var unitMarkOffset: CGFloat { get }
44 | var halfMarkOffset: CGFloat { get }
45 | var fractionMarkOffset: CGFloat { get }
46 | }
47 |
48 | extension ScaleView {
49 | var body: some View {
50 | shape
51 | .frame(size: .init(width: width, height: height))
52 | .fixedSize()
53 | }
54 |
55 | var unitMarkOffset: CGFloat { 0 }
56 | var halfMarkOffset: CGFloat { 0 }
57 | var fractionMarkOffset: CGFloat { 0 }
58 |
59 | static func ==(lhs: Self, rhs: Self) -> Bool {
60 | lhs.width == rhs.width && lhs.height == rhs.height
61 | }
62 | }
63 |
64 |
65 |
--------------------------------------------------------------------------------
/Sources/SlidingRuler/Styling/Protocols/SlidingRulerStyle.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SlidingRulerStyle.swift
3 | //
4 | // SlidingRuler
5 | //
6 | // MIT License
7 | //
8 | // Copyright (c) 2020 Pierre Tacchi
9 | //
10 | // Permission is hereby granted, free of charge, to any person obtaining a copy
11 | // of this software and associated documentation files (the "Software"), to deal
12 | // in the Software without restriction, including without limitation the rights
13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14 | // copies of the Software, and to permit persons to whom the Software is
15 | // furnished to do so, subject to the following conditions:
16 | //
17 | // The above copyright notice and this permission notice shall be included in all
18 | // copies or substantial portions of the Software.
19 | //
20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26 | // SOFTWARE.
27 | //
28 |
29 |
30 | import SwiftUI
31 |
32 | public protocol SlidingRulerStyle {
33 | associatedtype CellBody: FractionableView
34 | associatedtype CursorBody: View
35 |
36 | var fractions: Int { get }
37 | var cellWidth: CGFloat { get }
38 | var cursorAlignment: VerticalAlignment { get }
39 | var hasMarks: Bool { get }
40 | var hasHalf: Bool { get }
41 | var supportsPointerInteraction: Bool { get }
42 |
43 | func makeCellBody(configuration: SlidingRulerStyleConfiguation) -> CellBody
44 | func makeCursorBody() -> CursorBody
45 | }
46 |
47 | public extension SlidingRulerStyle {
48 | var fractions: Int { CellBody.fractions }
49 | var cellWidth: CGFloat { 120 }
50 | var hasMarks: Bool { true }
51 | var hasHalf: Bool { CellBody.hasHalf }
52 | var supportsPointerInteraction: Bool { true }
53 | }
54 |
--------------------------------------------------------------------------------
/Sources/SlidingRuler/Styling/StyleConfiguation.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StyleConfiguation.swift
3 | //
4 | // SlidingRuler
5 | //
6 | // MIT License
7 | //
8 | // Copyright (c) 2020 Pierre Tacchi
9 | //
10 | // Permission is hereby granted, free of charge, to any person obtaining a copy
11 | // of this software and associated documentation files (the "Software"), to deal
12 | // in the Software without restriction, including without limitation the rights
13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14 | // copies of the Software, and to permit persons to whom the Software is
15 | // furnished to do so, subject to the following conditions:
16 | //
17 | // The above copyright notice and this permission notice shall be included in all
18 | // copies or substantial portions of the Software.
19 | //
20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26 | // SOFTWARE.
27 | //
28 |
29 |
30 | import Foundation
31 | import CoreGraphics
32 |
33 | public struct SlidingRulerStyleConfiguation {
34 | let mark: CGFloat
35 | let bounds: ClosedRange
36 | let step: CGFloat
37 | let formatter: NumberFormatter?
38 | }
39 |
--------------------------------------------------------------------------------
/Sources/SlidingRuler/Utils.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Utils.swift
3 | //
4 | // SlidingRuler
5 | //
6 | // MIT License
7 | //
8 | // Copyright (c) 2020 Pierre Tacchi
9 | //
10 | // Permission is hereby granted, free of charge, to any person obtaining a copy
11 | // of this software and associated documentation files (the "Software"), to deal
12 | // in the Software without restriction, including without limitation the rights
13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14 | // copies of the Software, and to permit persons to whom the Software is
15 | // furnished to do so, subject to the following conditions:
16 | //
17 | // The above copyright notice and this permission notice shall be included in all
18 | // copies or substantial portions of the Software.
19 | //
20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26 | // SOFTWARE.
27 | //
28 |
29 | import SwiftUI
30 |
31 | func NextLoop(_ perform: @escaping () -> ()) {
32 | DispatchQueue.main.async(execute: perform)
33 | }
34 |
35 | func withoutAnimation(_ body: () throws -> Result) rethrows -> Result {
36 | try withAnimation(nil, body)
37 | }
38 |
--------------------------------------------------------------------------------
/Sources/SlidingRuler/VSynchedTimer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // VSynchedTimer.swift
3 | //
4 | // SlidingRuler
5 | //
6 | // MIT License
7 | //
8 | // Copyright (c) 2020 Pierre Tacchi
9 | //
10 | // Permission is hereby granted, free of charge, to any person obtaining a copy
11 | // of this software and associated documentation files (the "Software"), to deal
12 | // in the Software without restriction, including without limitation the rights
13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14 | // copies of the Software, and to permit persons to whom the Software is
15 | // furnished to do so, subject to the following conditions:
16 | //
17 | // The above copyright notice and this permission notice shall be included in all
18 | // copies or substantial portions of the Software.
19 | //
20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26 | // SOFTWARE.
27 | //
28 |
29 | import UIKit
30 |
31 | struct VSynchedTimer {
32 | typealias Animations = (TimeInterval, TimeInterval) -> ()
33 | typealias Completion = (Bool) -> ()
34 |
35 | private let timer: SynchedTimer
36 |
37 | init(duration: TimeInterval, animations: @escaping Animations, completion: Completion? = nil) {
38 | self.timer = .init(duration, animations, completion)
39 | }
40 |
41 | func cancel() { timer.cancel() }
42 | }
43 |
44 |
45 | private final class SynchedTimer {
46 | private let duration: TimeInterval
47 | private let animationBlock: VSynchedTimer.Animations
48 | private let completionBlock: VSynchedTimer.Completion?
49 | private weak var displayLink: CADisplayLink?
50 |
51 | private var isRunning: Bool
52 | private let startTimeStamp: TimeInterval
53 | private var lastTimeStamp: TimeInterval
54 |
55 | deinit {
56 | self.displayLink?.invalidate()
57 | }
58 |
59 | init(_ duration: TimeInterval, _ animations: @escaping VSynchedTimer.Animations, _ completion: VSynchedTimer.Completion? = nil) {
60 | self.duration = duration
61 | self.animationBlock = animations
62 | self.completionBlock = completion
63 |
64 | self.isRunning = true
65 | self.startTimeStamp = CACurrentMediaTime()
66 | self.lastTimeStamp = startTimeStamp
67 | self.displayLink = self.createDisplayLink()
68 | }
69 |
70 | func cancel() {
71 | guard isRunning else { return }
72 |
73 | isRunning.toggle()
74 | displayLink?.invalidate()
75 | self.completionBlock?(false)
76 | }
77 |
78 | private func complete() {
79 | guard isRunning else { return }
80 |
81 | isRunning.toggle()
82 | displayLink?.invalidate()
83 | self.completionBlock?(true)
84 | }
85 |
86 | @objc private func displayLinkTick(_ displayLink: CADisplayLink) {
87 | guard isRunning else { return }
88 |
89 | let currentTimeStamp = CACurrentMediaTime()
90 | let progress = currentTimeStamp - startTimeStamp
91 | let elapsed = currentTimeStamp - lastTimeStamp
92 | lastTimeStamp = currentTimeStamp
93 |
94 | if progress < duration {
95 | animationBlock(progress, elapsed)
96 | } else {
97 | complete()
98 | }
99 | }
100 |
101 | private func createDisplayLink() -> CADisplayLink {
102 | let dl = CADisplayLink(target: self, selector: #selector(displayLinkTick(_:)))
103 | dl.add(to: .main, forMode: .common)
104 |
105 | return dl
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/Tests/LinuxMain.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | import SlidingRulerTests
4 |
5 | var tests = [XCTestCaseEntry]()
6 | tests += SlidingRulerTests.allTests()
7 | XCTMain(tests)
8 |
--------------------------------------------------------------------------------
/Tests/SlidingRulerTests/SlidingRulerTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import SlidingRuler
3 |
4 | final class SlidingRulerTests: XCTestCase {
5 | func testExample() {
6 | // This is an example of a functional test case.
7 | // Use XCTAssert and related functions to verify your tests produce the correct
8 | // results.
9 | }
10 |
11 | static var allTests = [
12 | ("testExample", testExample),
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/Tests/SlidingRulerTests/XCTestManifests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | #if !canImport(ObjectiveC)
4 | public func allTests() -> [XCTestCaseEntry] {
5 | return [
6 | testCase(SlidingRulerTests.allTests),
7 | ]
8 | }
9 | #endif
10 |
--------------------------------------------------------------------------------