├── 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 | ![](https://github.com/matteofontana-app/Assets/blob/main/OneFingerRotation/MainTitle.gif) 2 | 3 | ![](https://img.shields.io/badge/Matteo%20Fontana-Framework-black.svg?style=flat-square&logo=data:image/png;base64,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) 4 | ![GitHub](https://img.shields.io/github/license/matteofontana-app/OneFingerRotation?label=License&style=flat-square) 5 | ![GitHub issues](https://img.shields.io/github/issues/matteofontana-app/OneFingerRotation?style=flat-square) 6 | ![GitHub Repo stars](https://img.shields.io/github/stars/matteofontana-app/OneFingerRotation?style=flat-square) 7 | ![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/matteofontana-app/OneFingerRotation?label=Package%20Size&style=flat-square) 8 | ![GitHub release (latest by date)](https://img.shields.io/github/v/release/matteofontana-app/OneFingerRotation?style=flat-square&label=Last%20Release)
9 | [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fmatteofontana-app%2FOneFingerRotation%2Fbadge%3Ftype%3Dswift-versions&style=flat-square)](https://swiftpackageindex.com/matteofontana-app/OneFingerRotation) 10 | [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fmatteofontana-app%2FOneFingerRotation%2Fbadge%3Ftype%3Dplatforms&style=flat-square)](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 | Buy Me A Coffee 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 | --------------------------------------------------------------------------------