├── Example.swift ├── LICENSE ├── README.md └── WiggleModifier.swift /Example.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct Example: View { 4 | @State private var isWiggling = false 5 | 6 | var body: some View { 7 | VStack { 8 | Rectangle() 9 | .frame(width: 100, height: 100) 10 | .foregroundColor(.blue) 11 | .wiggling(isWiggling: $isWiggling, bounceAmount: 2, rotationAmount: 3) 12 | // .wiggling(isWiggling: $isWiggling) we can also leave bounceAmount and rotationAmount blank, as the default values are 3 and 1 respectively 13 | 14 | Button(action: { 15 | withAnimation { 16 | self.isWiggling.toggle() 17 | } 18 | }) { 19 | Text(self.isWiggling ? "Disable Animation" : "Enable Animation") 20 | } 21 | .padding() 22 | .foregroundColor(.white) 23 | .background(Color.blue) 24 | .cornerRadius(10) 25 | } 26 | } 27 | } 28 | 29 | struct ContentView_Previews: PreviewProvider { 30 | static var previews: some View { 31 | ContentView() 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Nicolas Gimelli 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SwiftUI Wiggle Animation Modifier 2 | 3 | This repository contains a custom SwiftUI modifier for adding a 'wiggle' animation to any SwiftUI `View`. The idea was inspired by [markmals](https://github.com/markmals) on GitHub. This implementation extends the original idea by allowing the animation to be controlled by a boolean state variable and providing custom rotation and bounce amounts. 4 | 5 | https://github.com/ngimelliUW/WiggleAnimationModifier/assets/47952124/150cbbef-aa51-41da-9cd9-92cf52a449ef 6 | 7 | ## Installation 8 | 9 | Just copy the `WiggleModifier.swift` file into your project. 10 | 11 | ## Usage 12 | 13 | In your SwiftUI `View`, you can use the `wiggling` modifier like this: 14 | 15 | ```swift 16 | struct ContentView: View { 17 | @State private var isWiggling = false 18 | 19 | var body: some View { 20 | VStack { 21 | Rectangle() 22 | .frame(width: 100, height: 100) 23 | .foregroundColor(.blue) 24 | .wiggling(isWiggling: $isWiggling, rotationAmount: 3, bounceAmount: 2) 25 | 26 | Button(action: { 27 | withAnimation { 28 | self.isWiggling.toggle() 29 | } 30 | }) { 31 | Text(self.isWiggling ? "Disable Animation" : "Enable Animation") 32 | } 33 | .padding() 34 | .foregroundColor(.white) 35 | .background(Color.blue) 36 | .cornerRadius(10) 37 | } 38 | } 39 | } 40 | ``` 41 | 42 | The `wiggling` modifier takes three arguments: 43 | 44 | 1. `isWiggling`: A `Binding` that controls whether the wiggle animation is active. 45 | 2. `bounceAmount`: A `Double` that controls the bounce amount of the wiggle animation. 46 | 3. `rotationAmount`: A `Double` that controls the rotation amount of the wiggle animation. 47 | 48 | ## Contributing 49 | 50 | Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. 51 | 52 | Please make sure to update tests as appropriate. 53 | 54 | ## License 55 | 56 | [MIT](https://choosealicense.com/licenses/mit/) 57 | -------------------------------------------------------------------------------- /WiggleModifier.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | // Utility struct for animation related functions 4 | struct AnimationUtils { 5 | static func wiggleAnimation(interval: TimeInterval, variance: Double) -> Animation { 6 | return Animation.easeInOut(duration: randomize(interval: interval, withVariance: variance)).repeatForever(autoreverses: true) 7 | } 8 | 9 | static func randomize(interval: TimeInterval, withVariance variance: Double) -> TimeInterval { 10 | let random = (Double(arc4random_uniform(1000)) - 500.0) / 500.0 11 | return interval + variance * random 12 | } 13 | } 14 | 15 | struct WiggleRotationModifier: ViewModifier { 16 | @Binding var isWiggling: Bool 17 | var rotationAmount: Double 18 | 19 | func body(content: Content) -> some View { 20 | content 21 | .rotationEffect(Angle(degrees: isWiggling ? rotationAmount : 0)) 22 | .animation(isWiggling ? AnimationUtils.wiggleAnimation(interval: 0.14, variance: 0.025) : .default, value: isWiggling) 23 | } 24 | } 25 | 26 | struct WiggleBounceModifier: GeometryEffect { 27 | var amount: Double 28 | var bounceAmount: Double 29 | 30 | var animatableData: Double { 31 | get { amount } 32 | set { amount = newValue } 33 | } 34 | 35 | func effectValue(size: CGSize) -> ProjectionTransform { 36 | let bounce = sin(.pi * 2 * animatableData) * bounceAmount 37 | let translationEffect = CGAffineTransform(translationX: 0, y: CGFloat(bounce)) 38 | return ProjectionTransform(translationEffect) 39 | } 40 | } 41 | 42 | extension View { 43 | func wiggling(isWiggling: Binding, rotationAmount: Double = 3, bounceAmount: Double = 1) -> some View { 44 | self 45 | .modifier(WiggleRotationModifier(isWiggling: isWiggling, rotationAmount: rotationAmount)) 46 | .modifier(WiggleBounceModifier(amount: isWiggling.wrappedValue ? 1 : 0, bounceAmount: bounceAmount)) 47 | .animation(isWiggling.wrappedValue ? AnimationUtils.wiggleAnimation(interval: 0.3, variance: 0.025).repeatForever(autoreverses: true) : .default, value: isWiggling.wrappedValue) 48 | } 49 | } 50 | --------------------------------------------------------------------------------