├── Sources
└── OneFingerRotation
│ ├── OneFingerRotation.swift
│ └── OFKnob.swift
├── .swiftpm
└── xcode
│ ├── package.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcuserdata
│ │ └── matteofontana.xcuserdatad
│ │ └── UserInterfaceState.xcuserstate
│ └── xcuserdata
│ └── matteofontana.xcuserdatad
│ └── xcschemes
│ └── xcschememanagement.plist
├── Tests
└── OneFingerRotationTests
│ └── OneFingerRotationTests.swift
├── Package.swift
├── LICENSE
└── README.md
/Sources/OneFingerRotation/OneFingerRotation.swift:
--------------------------------------------------------------------------------
1 | public struct OneFingerRotation {
2 | public private(set) var text = "Hello, Rotation!"
3 |
4 | public init() {
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/xcuserdata/matteofontana.xcuserdatad/UserInterfaceState.xcuserstate:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mttfntn/OneFingerRotation/HEAD/.swiftpm/xcode/package.xcworkspace/xcuserdata/matteofontana.xcuserdatad/UserInterfaceState.xcuserstate
--------------------------------------------------------------------------------
/Tests/OneFingerRotationTests/OneFingerRotationTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import OneFingerRotation
3 |
4 | final class OneFingerRotationTests: XCTestCase {
5 | func testExample() throws {
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 | XCTAssertEqual(OneFingerRotation().text, "Hello, World!")
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/xcuserdata/matteofontana.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | OneFingerRotation.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 0
11 |
12 |
13 | SuppressBuildableAutocreation
14 |
15 | OneFingerRotation
16 |
17 | primary
18 |
19 |
20 | OneFingerRotationTests
21 |
22 | primary
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.6
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: "OneFingerRotation",
8 | platforms: [
9 | .iOS(.v14), .macOS(.v11), .watchOS(.v7)
10 | ],
11 | products: [
12 | // Products define the executables and libraries a package produces, and make them visible to other packages.
13 | .library(
14 | name: "OneFingerRotation",
15 | targets: ["OneFingerRotation"]),
16 | ],
17 | targets: [
18 | // Targets are the basic building blocks of a package. A target can define a module or a test suite.
19 | // Targets can depend on other targets in this package, and on products in packages this package depends on.
20 | .target(
21 | name: "OneFingerRotation",
22 | dependencies: []),
23 | .testTarget(
24 | name: "OneFingerRotationTests",
25 | dependencies: ["OneFingerRotation"]),
26 | ]
27 | )
28 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Matteo Fontana
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Sources/OneFingerRotation/OFKnob.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KnobRotation.swift
3 | // OneFingerRotation Knob
4 | //
5 | // Created by Matteo Fontana on 23/04/23.
6 | //
7 |
8 | import SwiftUI
9 |
10 | public struct OFKnob: ViewModifier {
11 |
12 |
13 | @State private var rotationAngle: Angle = .degrees(0)
14 | @Binding var knobValue: Double
15 | @GestureState private var gestureRotation: Angle = .zero
16 | @State private var lastVelocity: CGFloat = 0
17 | @State private var isSpinning = false
18 | @State private var timer: Timer?
19 | @Binding var friction: CGFloat
20 | @Binding var stoppingAnimation: Bool
21 | @Binding var velocityMultiplier: CGFloat
22 | @State private var viewSize: CGSize = .zero
23 | var animation: Animation?
24 | @State private var isDragged: Bool = false
25 | let rotationThreshold: CGFloat = 12.0
26 | var onKnobValueChanged: (Double) -> Void
27 | @State var totalAngle: Double
28 | @State private var previousAngle: Double = 0
29 | @State private var rotationDirection: Double = 1
30 | @State var minAngle: Double
31 | @State var maxAngle: Double
32 |
33 |
34 | /// Initialization of three declarable and optional values.
35 | public init(knobValue: Binding,
36 | minAngle: Double, maxAngle: Double,
37 | friction: Binding = .constant(0.1),
38 | velocityMultiplier: Binding = .constant(0.1),
39 | rotationAngle: Angle = .degrees(0.0),
40 | animation: Animation? = nil,
41 | onKnobValueChanged: @escaping (Double) -> Void,
42 | stoppingAnimation: Binding = .constant(false)
43 | ){
44 | self._knobValue = knobValue
45 | self.minAngle = minAngle
46 | self.maxAngle = maxAngle
47 | self._friction = friction
48 | self._velocityMultiplier = velocityMultiplier
49 | self.rotationAngle = Angle(degrees: minAngle+(maxAngle-minAngle)*knobValue.wrappedValue)
50 | self.onKnobValueChanged = onKnobValueChanged
51 | self.animation = animation
52 | self.totalAngle = minAngle+(maxAngle-minAngle)*knobValue.wrappedValue
53 | self._stoppingAnimation = stoppingAnimation
54 | }
55 |
56 | public func body(content: Content) -> some View {
57 | GeometryReader { geometry in
58 |
59 | content
60 |
61 | /// The ".background" modifier and the ".onPreferenceChange" update the automatic frame calculation of the content.
62 | .background(
63 | GeometryReader { geometry in
64 | Color.clear.preference(key: FrameSizeKeyKnobInertia.self, value: geometry.size)
65 | }
66 | )
67 | .onPreferenceChange(FrameSizeKeyKnobInertia.self) { newSize in
68 | viewSize = newSize
69 | }
70 | /// The ".position" modifier fix the center of the content.
71 | .position(x: geometry.size.width / 2, y: geometry.size.height / 2)
72 |
73 | /// The ".rotationEffect" modifier is necessary for the gesture functions, it applies the specific rotation.
74 | .rotationEffect(rotationAngle + gestureRotation, anchor: .center)
75 |
76 | .onChange(of: knobValue) { newValue in
77 | if !isDragged && isSpinning {
78 | if let animation = animation {
79 | withAnimation(animation) {
80 | rotationAngle = Angle(degrees: minAngle+(maxAngle-minAngle)*newValue)
81 | if stoppingAnimation {
82 | timer?.invalidate()
83 | isSpinning = false
84 | }
85 | stoppingAnimation = false
86 | }
87 | }
88 | else {
89 | rotationAngle = Angle(degrees: minAngle+(maxAngle-minAngle)*newValue)
90 | if stoppingAnimation {
91 | timer?.invalidate()
92 | isSpinning = false
93 | }
94 | stoppingAnimation = false
95 | }
96 | }
97 | if !isDragged && !isSpinning {
98 | if let animation = animation {
99 | withAnimation(animation) {
100 | rotationAngle = Angle(degrees: minAngle+(maxAngle-minAngle)*newValue)
101 | }
102 | } else {
103 | rotationAngle = Angle(degrees: minAngle+(maxAngle-minAngle)*newValue)
104 | }
105 | }
106 | }
107 |
108 | .onChange(of: stoppingAnimation) { newValue in
109 | if !isSpinning && !isDragged {
110 | stoppingAnimation = false
111 | }
112 | }
113 |
114 | /// The ".gesture" modifier is necessary for the gesture functions.
115 | .gesture(
116 | DragGesture(minimumDistance: 0)
117 | .onChanged { value in
118 | isDragged = true
119 | timer?.invalidate()
120 | let dragAngle = calculateRotationAngle(value: value, geometry: geometry)
121 | var angleDifference = dragAngle.degrees - previousAngle
122 |
123 | // Handle angle difference when crossing the ±180 boundary
124 | if abs(angleDifference) > 180 {
125 | angleDifference = angleDifference > 0 ? angleDifference - 360 : angleDifference + 360
126 | }
127 |
128 | // Determine rotation direction
129 | rotationDirection = angleDifference >= 0 ? 1 : -1
130 |
131 | let currentAngle = rotationAngle.degrees + angleDifference
132 | rotationAngle = Angle(degrees: currentAngle)
133 | let clampedAngle = min(max(minAngle, currentAngle), maxAngle)
134 |
135 | if abs(angleDifference) < 90 { // Add this line to check the angleDifference threshold
136 | if minAngle...maxAngle ~= clampedAngle {
137 | rotationAngle = Angle(degrees: clampedAngle)
138 | knobValue = (clampedAngle - minAngle) / (maxAngle - minAngle)
139 | }
140 | }
141 |
142 | previousAngle = dragAngle.degrees
143 |
144 | // Update totalAngle without adding fullRotations * 360
145 | totalAngle += angleDifference
146 | onKnobValueChanged(knobValue)
147 | }
148 | .onEnded { value in
149 | previousAngle = 0
150 | isDragged = false
151 | isSpinning = true
152 | rotationAngle += gestureRotation
153 | let velocity = CGPoint(x: value.predictedEndLocation.x - value.location.x, y: value.predictedEndLocation.y - value.location.y)
154 | lastVelocity = sqrt(pow(velocity.x, 2) + pow(velocity.y, 2)) * velocityMultiplier
155 | onKnobValueChanged(knobValue)
156 |
157 | if abs(velocity.x) > rotationThreshold || abs(velocity.y) > rotationThreshold {
158 | isSpinning = true
159 | timer?.invalidate()
160 | timer = Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true) { timer in
161 | let angle = Angle(degrees: Double(lastVelocity) * rotationDirection)
162 | let newRotationAngle = rotationAngle + angle
163 | let clampedAngle = min(max(minAngle, newRotationAngle.degrees), maxAngle)
164 | if abs(newRotationAngle.degrees - clampedAngle) > 0.1 {
165 | let deceleration = 0.2 * rotationDirection
166 | rotationAngle = Angle(degrees: clampedAngle + deceleration)
167 | knobValue = (rotationAngle.degrees - minAngle) / (maxAngle - minAngle) // Update knobValue here
168 | onKnobValueChanged(knobValue)
169 | lastVelocity *= (1 - friction)
170 | if lastVelocity < 0.1 {
171 | timer.invalidate()
172 | isSpinning = false
173 | }
174 | } else {
175 | rotationAngle = newRotationAngle
176 | knobValue = (rotationAngle.degrees - minAngle) / (maxAngle - minAngle) // Update knobValue here
177 | onKnobValueChanged(knobValue)
178 | lastVelocity *= (1 - friction)
179 | if lastVelocity < 0.1 || (newRotationAngle.degrees < minAngle || newRotationAngle.degrees > maxAngle) {
180 | timer.invalidate()
181 | isSpinning = false
182 | }
183 | }
184 | }
185 | } else {
186 | timer?.invalidate()
187 | isSpinning = false
188 | }
189 | }
190 | )
191 |
192 |
193 | /// The ".onAppear" modifier is necessary for the gesture functions.
194 | .onAppear {
195 | timer?.invalidate()
196 | }
197 |
198 | /// The ".onDisappear" modifier is necessary for the gesture functions.
199 | .onDisappear {
200 | timer?.invalidate()
201 | }
202 | }
203 |
204 | /// This ".frame" modifier ensures that the content is at the center of the view always.
205 | .frame(width: viewSize.width, height: viewSize.height)
206 | }
207 |
208 | /// The function calculateRotationAngle calculates the angle according to the finger movement.
209 | public func calculateRotationAngle(value: DragGesture.Value, geometry: GeometryProxy) -> Angle {
210 | let centerX = value.startLocation.x - geometry.size.width / 2
211 | let centerY = value.startLocation.y - geometry.size.height / 2
212 | let startVector = CGVector(dx: centerX, dy: centerY)
213 | let endX = value.startLocation.x + value.translation.width - geometry.size.width / 2
214 | let endY = value.startLocation.y + value.translation.height - geometry.size.height / 2
215 | let endVector = CGVector(dx: endX, dy: endY)
216 | let angleDifference = atan2(startVector.dy * endVector.dx - startVector.dx * endVector.dy, startVector.dx * endVector.dx + startVector.dy * endVector.dy)
217 | return Angle(radians: -Double(angleDifference))
218 | }
219 | }
220 |
221 | /// This PreferenceKey is necessary for the calculation of the frame width and height of the content.
222 | struct FrameSizeKeyKnobInertia: PreferenceKey {
223 | static var defaultValue: CGSize = .zero
224 |
225 | static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
226 | value = nextValue()
227 | }
228 | }
229 |
230 | public extension View {
231 | func oFKnob(
232 | knobValue: Binding,
233 | minAngle: Double? = nil,
234 | maxAngle: Double? = nil,
235 | friction: Binding? = nil,
236 | onKnobValueChanged: @escaping (Double) -> Void,
237 | velocityMultiplier: Binding? = nil,
238 | animation: Animation? = nil,
239 | stoppingAnimation: Binding? = nil) -> some View
240 | {
241 | let effect = OFKnob(
242 | knobValue: knobValue,
243 | minAngle: minAngle ?? -90,
244 | maxAngle: maxAngle ?? 90,
245 | friction: friction ?? .constant(0.1),
246 | velocityMultiplier: velocityMultiplier ?? .constant(0.1),
247 | animation: animation,
248 | onKnobValueChanged: onKnobValueChanged,
249 | stoppingAnimation: stoppingAnimation ?? .constant(false)
250 | )
251 | return self.modifier(effect)
252 | }
253 | }
254 |
255 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | 
4 | 
5 | 
6 | 
7 | 
8 | 
9 | [](https://swiftpackageindex.com/matteofontana-app/OneFingerRotation)
10 | [](https://swiftpackageindex.com/matteofontana-app/OneFingerRotation)
11 |
12 |
13 | OneFingerRotation is a lightweight **SwiftUI framework** that enables you to add a one-finger rotation gesture to any view with a single modifier. This library is perfect for developers who want to quickly and easily implement **rotation functionality** in their SwiftUI applications without the hassle of dealing with complex gesture recognizers.
14 |
15 | Current Version: **1.2.0**
16 |
17 | ---
18 |
19 |
20 |
21 |
22 |
23 | ## Table of Content
24 |
25 |
26 |
27 | - [Table of Content](#table-of-content)
28 | - [Presentation](#presentation)
29 | - [Usage](#usage)
30 | - [Simple Rotation](#simple-rotation)
31 | - [Simple Rotation with Inertia](#simple-rotation-with-inertia)
32 | - [Value Rotation](#value-rotation)
33 | - [Value Rotation with Inertia](#value-rotation-with-inertia)
34 | - [Auto Rotation](#auto-rotation)
35 | - [Value Auto Rotation](#value-auto-rotation)
36 | - [Value Auto Rotation with Inertia](#value-auto-rotation-with-inertia)
37 | - [Knob](#knob)
38 | - [Knob with Inertia](#knob-with-inertia)
39 | - [Examples](#examples)
40 | - [Versions](#versions)
41 | - [Contributions](#contributions)
42 | - [Next to come:](#next-to-come)
43 | - [Donations / Support](#donations--support)
44 | - [Contact](#contact)
45 | - [License](#license)
46 |
47 |
48 | ## Usage
49 |
50 | The Framework is composed of a series of modifiers that you can apply to any view in your SwiftUI project.
51 | It is recommended to use a frame modifier with equal width and height above the frameworks modifiers, this will ensure that the rotation will take place inside of the specific view. Example:
52 |
53 | ```swift
54 | .frame(width: 400, height: 400)
55 | ```
56 | Use only one modifier of the framework at a time.
57 | Here's the list of the modifiers in the framework:
58 |
59 |
60 |
61 |
62 | ### Simple Rotation
63 |
64 |
65 |
66 | The Simple Rotation allows for a simple rotation using one finger.
67 |
68 |
69 |
70 | **Declaration:**
71 |
72 | ```swift
73 | .simpleRotation()
74 | ```
75 | **Customization:**
76 |
77 | * rotationAngle: identifies the original angle of the element
[Type: **Angle** - Stock value: **.degrees(0)**]
78 |
79 | * angleSnap: allows snapping using an angle factor which identifies the basic multiple of the angle
[Type: **Binding\** - Stock Value: **Non declared**]
80 |
81 | ```swift
82 | .simpleRotation(
83 | rotationAngle: .degrees(20)
84 | angleSnap: .constant(60)
85 | )
86 | ```
87 |
88 |
89 |
90 |
91 |
92 |
93 | ### Simple Rotation with Inertia
94 |
95 |
96 |
97 | The Simple Inertia Rotation allows the view for a simple rotation with consequent inertia effect.
98 |
99 |
100 |
101 | **Declaration:**
102 |
103 | Declaration should be like this:
104 |
105 | ```swift
106 | .simpleRotationInertia()
107 | ```
108 |
109 | **Customization:**
110 |
111 | These are the customization possibilities:
112 | * friction: the inertia factor of slowdown.
[Type: **Binding\** - Stock value: **0.005**]
113 | * velocityMultiplier: the speed multiplier of the inertia function related to the speed of the gesture on screen.
[Type: **Binding\** - Stock value: **0.1**]
114 | * decelerationFactor: the deceleration factor multiplier, big value indicates a longer inertia.
[Type: **Binding\** - Stock value: **0.4**]
115 | * rotationAngle: identifies the original angle of the element
[Type: **Angle** - Stock value: **.degrees(0)**]
116 | * angleSnap: allows snapping using an angle factor which identifies the basic multiple of the angle
[Type: **Binding\** - Stock Value: **Non declared**]
117 | * angleSnapShowFactor: this variable controls the visibility during the inertia of angles not belonging to the angleSnap range.
[Type: **Binding\** - Stock value: **0.1**]
118 |
119 | ```swift
120 | .simpleRotationInertia(
121 | friction: .constant(0.005),
122 | velocityMultiplier: .constant(0.1),
123 | decelerationFactor: .constant(0.4),
124 | rotationAngle: .degrees(0.0),
125 | angleSnap: .constant(20),
126 | angleSnapShowFactor: .constant(0.1)
127 | )
128 | ```
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 | ### Value Rotation
137 |
138 |
139 | The Value Rotation allows for a simple rotation using one finger with a linked value related to the total angle of rotation.
140 |
141 |
142 |
143 | **Setup**
144 |
145 | To use the modifier it's necessary to create a variable of type double that will indicate the starting point of the element. Example:
146 |
147 | ```swift
148 | @State private var totalAngle: Double = 0.0
149 | ```
150 |
151 | **Declaration:**
152 |
153 | To declare the modifier it is mandatory to link the variable and also the onAngleChanged, inside of this there must be the linked variable.
154 |
155 | ```swift
156 | .valueRotation(
157 | totalAngle: $totalAngle2,
158 | onAngleChanged: { newAngle in
159 | totalAngle2 = newAngle
160 | }
161 | )
162 | ```
163 |
164 | **Customization:**
165 |
166 | * animation: this parameter controls the animation during a change of totalAngle from outside the modifier.
[Type: **Animation** - Stock value: **Missing value**]
167 |
168 | ```swift
169 | .valueRotation(
170 | totalAngle: $totalAngle,
171 | onAngleChanged: { newAngle in
172 | totalAngle = newAngle
173 | },
174 | animation: .spring()
175 | )
176 | ```
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 | ### Value Rotation with Inertia
186 |
187 |
188 |
189 | The Value Rotation with Inertia allows for a rotation with value linked and inertia effect at the end of the gesture.
190 |
191 |
192 |
193 | **Setup**
194 |
195 | To use the modifier it's necessary to create a variable of type double that will indicate the starting point of the element. Example:
196 |
197 | ```swift
198 | @State private var totalAngle: Double = 0.0
199 | ```
200 |
201 | **Declaration:**
202 |
203 | To declare the modifier it is mandatory to link the variable and also the onAngleChanged, inside of this there must be the linked variable.
204 |
205 | ```swift
206 | .valueRotationInertia(
207 | totalAngle: $totalAngle,
208 | onAngleChanged: { newAngle in
209 | totalAngle = newAngle
210 | }
211 | )
212 | ```
213 |
214 |
215 | **Customization:**
216 |
217 | These are the customization possibilities:
218 | * friction: the inertia factor of slowdown.
[Type: **Binding\** - Stock value: **0.005**]
219 | * velocityMultiplier: the speed multiplier of the inertia function related to the speed of the gesture on screen.
[Type: **Binding\** - Stock value: **0.1**]
220 | * animation: this parameter controls the animation during a change of totalAngle from outside the modifier.
[Type: **Animation** - Stock value: **Missing**]
221 | * stoppingAnimation: this variable controls if the rotation stops after the value of knobValue changes outside the modifier. It is suggested to use a variable as it will be needed in case of this application.
[Type: **Binding\** - Stock value: **false**]
222 |
223 | ```swift
224 | .valueRotationInertia(
225 | totalAngle: $totalAngle,
226 | friction: .constant(0.005),
227 | onAngleChanged: { newAngle in
228 | totalAngle3 = newAngle
229 | },
230 | velocityMultiplier: .constant(0.1),
231 | animation: .spring(),
232 | stoppingAnimation: $valueChange
233 | )
234 | ```
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 | ### Auto Rotation
243 |
244 |
245 |
246 | The Auto Rotation applies an automatic rotation to a simple rotation.
247 |
248 |
249 |
250 | **Declaration:**
251 |
252 | ```swift
253 | .autoRotation()
254 | ```
255 | **Customization:**
256 |
257 | * rotationAngle: Identifies the original angle of the element
[Type: **Angle** - Stock value: **.degrees(0)**]
258 |
259 | * autoRotationSpeed: Indicates the speed of the rotation of the content during motion.
[Type: **Binding\** - Stock value: **20**]
260 |
261 | * autoRotationActive: Indicates if the content has to rotate or not, allowing for pause of the rotation.
[Type: **Binding\** - Stock value: **true**]
262 |
263 | ```swift
264 | .autoRotation(
265 | rotationAngle: .degrees(20)
266 | autoRotationSpeed: .constant(20),
267 | autoRotationActive: .constant(true)
268 | )
269 | ```
270 |
271 | In case there have been use of variables for the last two parameters it is possible to modify them using binding variables:
272 |
273 | ```swift
274 | .autoRotation(
275 | rotationAngle: .degrees(20),
276 | autoRotationSpeed: $autoRotationSpeed,
277 | autoRotationActive: $autoRotationActive
278 | )
279 | ```
280 |
281 | Using this method will make able to modify the variables during the use of the modifier:
282 |
283 | ```swift
284 | Button(action: {
285 | autoRotationActive.toggle()
286 | }, label: {
287 | Text("Pause the Rotation")
288 | })
289 |
290 | Button(action: {
291 | autoRotationSpeed = [Insert double value here]
292 | }, label: {
293 | Text("Modify the speed")
294 | })
295 | ```
296 |
297 |
298 |
299 |
300 |
301 |
302 |
303 | ### Value Auto Rotation
304 |
305 |
306 |
307 | The Value Auto Rotation links a value related to the angle of an Auto Rotation
308 |
309 |
310 |
311 | **Setup**
312 |
313 | To use the modifier it's necessary to create a variable of type double that will indicate the starting point of the element. Example:
314 |
315 | ```swift
316 | @State private var totalAngle: Double = 0.0
317 | ```
318 |
319 | **Declaration:**
320 |
321 | To declare the modifier it is mandatory to link the variable and also the onAngleChanged, inside of this there must be the linked variable.
322 |
323 | ```swift
324 | .valueAutoRotation(
325 | totalAngle: $totalAngle,
326 | onAngleChanged: { newAngle in
327 | totalAngle = newAngle
328 | }
329 | )
330 | ```
331 |
332 | **Customization:**
333 |
334 | * animation: this parameter controls the animation during a change of totalAngle from outside the modifier.
[Type: **Animation** - Stock value: **Missing value**]
335 |
336 | * autoRotationSpeed: Indicates the speed of the rotation of the content during motion.
[Type: **Binding\** - Stock value: **20**]
337 |
338 | * autoRotationActive: Indicates if the content has to rotate or not, allowing for pause of the rotation.
[Type: **Binding\** - Stock value: **true**]
339 |
340 | ```swift
341 | .valueAutoRotation(
342 | totalAngle: $totalAngle,
343 | onAngleChanged: { newAngle in
344 | totalAngle = newAngle
345 | },
346 | animation: .spring(),
347 | autoRotationSpeed: .constant(20),
348 | autoRotationEnabled: .constant(true)
349 | )
350 | ```
351 |
352 | At this point is also possible to add the reading of the totalAngle:
353 | ```swift
354 | Text("The value is: \(totalAngle)")
355 | ```
356 |
357 | In case there have been use of variables for the last two parameters it is possible to modify them using binding variables:
358 |
359 | ```swift
360 | .valueAutoRotation(
361 | totalAngle: $totalAngle,
362 | onAngleChanged: { newAngle in
363 | totalAngle = newAngle
364 | },
365 | animation: .spring(),
366 | autoRotationSpeed: $autoRotationSpeed,
367 | autoRotationActive: $autoRotationActive
368 | )
369 | ```
370 |
371 | Using this method will make able to modify the variables during the use of the modifier:
372 |
373 | ```swift
374 | Button(action: {
375 | autoRotationActive.toggle()
376 | }, label: {
377 | Text("Pause the Rotation")
378 | })
379 |
380 | Button(action: {
381 | autoRotationSpeed = [Insert double value here]
382 | }, label: {
383 | Text("Modify the speed")
384 | })
385 | ```
386 |
387 |
388 | content
389 |
390 |
391 |
392 |
393 |
394 |
395 | ### Value Auto Rotation with Inertia
396 |
397 |
398 |
399 | An Automatic rotation with finger rotation gesture and inertia effect. All in one.
400 |
401 |
402 |
403 | **Setup**
404 |
405 | To use the modifier it's necessary to create a variable of type double that will indicate the starting point of the element. Example:
406 |
407 | ```swift
408 | @State private var totalAngle: Double = 0.0
409 | ```
410 |
411 | **Declaration:**
412 |
413 | To declare the modifier it is mandatory to link the variable and also the onAngleChanged, inside of this there must be the linked variable.
414 |
415 | ```swift
416 | .valueAutoRotationInertia(
417 | totalAngle: $totalAngle,
418 | onAngleChanged: { newAngle in
419 | totalAngle = newAngle
420 | }
421 | )
422 | ```
423 |
424 | **Customization**
425 |
426 | These are the customization possibilities:
427 |
428 | * friction: the inertia factor of slowdown.
[Type: **Binding\** - Stock value: **0.005**]
429 | * velocityMultiplier: the speed multiplier of the inertia function related to the speed of the gesture on screen.
[Type: **Binding\** - Stock value: **0.1**]
430 | * animation: this parameter controls the animation during a change of totalAngle from outside the modifier.
[Type: **Animation** - Stock value: **Missing value**]
431 | * stoppingAnimation: this variable controls if the rotation stops after the value of knobValue changes outside the modifier. It is suggested to use a variable as it will be needed in case of this application.
[Type: **Binding\** - Stock value: false]
432 | * autoRotationSpeed: Indicates the speed of the rotation of the content during motion.
[Type: **Binding\** - Stock value: **20**]
433 | * autoRotationActive: Indicates if the content has to rotate or not, allowing for pause of the rotation.
[Type: **Binding\** - Stock value: **true**]
434 |
435 | ```swift
436 | .valueAutoRotationInertia(
437 | totalAngle: $totalAngle,
438 | friction: .constant(0.1)
439 | onAngleChanged: { newAngle in
440 | totalAngle = newAngle
441 | },
442 | velocityMultiplier: .constant(0.1),
443 | animation: .spring(),
444 | stoppingAnimation: $valueChange,
445 | autoRotationSpeed: .constant(90),
446 | autoRotationEnabled: .constant(true)
447 | )
448 | ```
449 |
450 |
451 |
452 |
453 |
454 |
455 |
456 | ### Knob
457 |
458 |
459 |
460 | The Knob applies a range value from 0 to 1 to a certain angle interval.
461 |
462 |
463 |
464 | **Setup:**
465 |
466 | To use the modifier it's necessary to create a variable of type double between the range 0.0-1.0, this variable will indicate the starting point of the knob. For example in this next implementation, the knob will start from the middle point:
467 |
468 | ```swift
469 | @State private var knobValue: Double = 0.5
470 | ```
471 |
472 | **Declaration:**
473 |
474 | To declare the modifier it is mandatory to link the variable and also the onKnobValueChanged, inside of this there must be the linked variable.
475 |
476 | ```swift
477 | .knobRotation(
478 | knobValue: $knobValue,
479 | onKnobValueChanged: { newValue in
480 | knobValue = newValue
481 | }
482 | )
483 | ```
484 |
485 | **Customization:**
486 |
487 | * minAngle: the minimum angle of the knob.
[Type: **Double** - Stock value: **-90**]
488 | * maxAngle: the maximum angle of the knob.
[Type: **Double** - Stock value: **+90**]
489 | * animation: this parameter controls the animation during a change of knobValue from outside the modifier.
[Type: **Animation** - Stock value: **Missing value**]
490 |
491 |
492 | ```swift
493 | .knobRotation(
494 | knobValue: $knobValue,
495 | minAngle: -180,
496 | maxAngle: +180,
497 | onKnobValueChanged: { newValue in
498 | knobValue = newValue
499 | },
500 | animation: .spring()
501 | )
502 | ```
503 | At this point is also possible to add the reading of the knobValue:
504 | ```swift
505 | Text("The value is: \(knobValue)")
506 | ```
507 | In case there is need to change the value from outside this is the procedure ot call it:
508 | ```swift
509 | Button(action: {
510 | knobValue = 0.6
511 | }, label: {
512 | Text("Button")
513 | })
514 | ```
515 |
516 |
517 |
518 |
519 |
520 |
521 |
522 | ### Knob with Inertia
523 |
524 |
525 |
526 | The Knob Inertia applies inertia effect to a simple knob.
527 |
528 |
529 |
530 | **Setup:**
531 |
532 | To use the modifier it's necessary to create a variable of type double between the range 0.0-1.0, this variable will indicate the starting point of the knob. For example in this next implementation, the knob will start from the middle point:
533 |
534 | ```swift
535 | @State private var knobValue: Double = 0.5
536 | ```
537 |
538 | **Declaration:**
539 |
540 | To declare the modifier it is mandatory to link the variable and also the onKnobValueChanged, inside of this there must be the linked variable.
541 |
542 | ```swift
543 | .knobInertia(
544 | knobValue: $knobValue,
545 | onKnobValueChanged: { newValue in
546 | knobValue = newValue
547 | }
548 | )
549 | ```
550 |
551 | **Customization:**
552 |
553 | You can customize these parameters:
554 | * minAngle: the minimum angle of the knob.
[Type: **Double** - Stock value: **-90**]
555 | * maxAngle: the maximum angle of the knob.
[Type: **Double** - Stock value: **+90**]
556 | * friction: the inertia factor of slowdown.
[Type: **Binding\** - Stock value: **0.2**]
557 | * velocityMultiplier: the speed multiplier of the inertia function related to the speed of the gesture on screen.
[Type: **Binding\** - Stock value: **0.1**]
558 | * animation: this parameter controls the animation during a change of knobValue from outside the modifier.
[Type: **Animation** - Stock value: **Missing value**]
559 | * stoppingAnimation: this variable controls if the rotation stops after the value of knobValue changes outside the modifier. It is suggested to use a variable as it will be needed in case of this application.
[Type: **Binding\** - Stock value: **false**]
560 |
561 |
562 | ```swift
563 | @State var valueChange: Bool = false
564 | ///Other code sections
565 | .knobInertia(
566 | knobValue: $knobValue,
567 | minangle: -180,
568 | maxAngle: +180,
569 | friction: .constant(0.1),
570 | onKnobValueChanged: { newValue in
571 | knobValue = newValue
572 | },
573 | velocityMultiplier: .constant(0.1),
574 | animation: .spring(),
575 | stoppingAnimation: $valueChange
576 | )
577 | ```
578 |
579 | At this point is also possible to add the reading of the knobValue:
580 | ```swift
581 | Text("The value is: \(knobValue)")
582 | ```
583 | In case there is need to change the value from outside this is the procedure ot call it:
Other than sending the new value it is necessary to switch the value of valueChange like this:
584 | ```swift
585 | Button(action: {
586 | knobValue = 0.6
587 | valueChange = true
588 | }, label: {
589 | Text("Button")
590 | })
591 | ```
592 |
593 |
594 |
595 | ## Versions
596 | * 1.2.1: Refinements to the content of the README file, adjustments to the Simple Rotation function, video examples in the README file.
597 | * 1.2.0: Implementation of functions of snapping for Simple Rotation, Simple Rotation with Inertia
598 | * 1.0.0: Implementation of the modifiers: Simple Rotation, Simple Rotation with Inertia, Value Rotation, Value Rotation with Inertia, Auto Rotation, Value Auto Rotation, Value Auto Rotation with Inertia, Knob, Knob with Inertia.
599 |
600 | ---
601 | ## Contributions
602 |
603 | Feel free to file a new issue with a respective title and description in the specific issue form. If you want you can also fill a pull request to speed up on the improvement of the framework. As the code is open source, feel free to work on the code as you like. Remember that a citation to this page and to the profile will be appreciated. Thank you!
604 |
605 | ---
606 |
607 | ## Next to come:
608 |
609 | The angleSnap functions can be added to all the modifiers. So this will be the purpose of future updates.
610 |
611 | ---
612 | ## Donations / Support
613 |
614 | You can support me and my work through buymeacoffee. Any help would be really appreciated!
615 |
616 |
617 |
618 | ---
619 | ## Contact
620 |
621 | In case you are interested in collaborations or commisions, you can contact me through these channels:
622 |
623 | Mail: matteofontana@matteofontana.app
624 | Website: matteofontana.app
625 |
626 | ---
627 | ## License
628 |
629 | MIT License
630 |
631 | Copyright © 2023 Matteo Fontana
632 |
633 | Permission is hereby granted, free of charge, to any person obtaining a copy
634 | of this software and associated documentation files (the "Software"), to deal
635 | in the Software without restriction, including without limitation the rights
636 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
637 | copies of the Software, and to permit persons to whom the Software is
638 | furnished to do so, subject to the following conditions:
639 |
640 | The above copyright notice and this permission notice shall be included in all
641 | copies or substantial portions of the Software.
642 |
643 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
644 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
645 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
646 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
647 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
648 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
649 | SOFTWARE.
650 |
651 | Copyright © 2023 Matteo Fontana
652 |
--------------------------------------------------------------------------------