├── .gitignore ├── Package.swift ├── README.md ├── Sources └── SpringAnimation │ ├── AnimationPreset.swift │ ├── DesignableButton.swift │ ├── Extension + UIColor.swift │ ├── SpringAnimation.swift │ ├── SpringButton.swift │ ├── SpringImageView.swift │ ├── SpringLabel.swift │ ├── SpringTextField.swift │ ├── SpringTextView.swift │ ├── SpringView.swift │ └── Springable.swift └── Tests └── SpringAnimationTests └── SpringAnimationTests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | -------------------------------------------------------------------------------- /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: "SpringAnimation", 8 | platforms: [.iOS(.v13)], 9 | products: [ 10 | // Products define the executables and libraries a package produces, and make them visible to other packages. 11 | .library( 12 | name: "SpringAnimation", 13 | targets: ["SpringAnimation"]), 14 | ], 15 | dependencies: [ 16 | // Dependencies declare other packages that this package depends on. 17 | // .package(url: /* package url */, from: "1.0.0"), 18 | ], 19 | targets: [ 20 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 21 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 22 | .target( 23 | name: "SpringAnimation", 24 | dependencies: []), 25 | .testTarget( 26 | name: "SpringAnimationTests", 27 | dependencies: ["SpringAnimation"]), 28 | ] 29 | ) 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SpringAnimation 2 | 3 | ## Requirements 4 | Requires Xcode 13 and Swift 5.6 5 | 6 | ## SpringApp 7 | To see all possible animations and curves in action download the SpringApp application: https://github.com/LexDeBash/SpringApp.git 8 | 9 | ## Installation 10 | ### Swift Package Manager 11 | Copy framework url to clipboard: 12 | 13 | ![image](https://user-images.githubusercontent.com/18059014/164895275-4f3ece51-f201-4c92-9a14-f5e49607eedb.png) 14 | 15 | Open your project in Xcode and go to the "Frameworks, Libraries and Embedded Content" section in the general project settings, then click on the add new library button: 16 | 17 | ![image](https://user-images.githubusercontent.com/18059014/164894607-c9b92f73-b900-4d9b-ab28-0b7d475eab5b.png) 18 | 19 | Then select "Add Other..." -> "Add Package Dependency...": 20 | 21 | ![image](https://user-images.githubusercontent.com/18059014/164894764-2c961e46-1d0b-4b8d-9e08-7b47f43e63ca.png) 22 | 23 | In the window that opens, go to the search bar and paste the url ```https://github.com/LexDeBash/SpringAnimation.git``` 24 | Then press the button "Add Package": 25 | 26 | ![image](https://user-images.githubusercontent.com/18059014/164895127-a6e388b6-4bdb-4fa8-a232-21ad7aff1ab7.png) 27 | 28 | In the package selection window, check the SptingAnimation and press "Add Package": 29 | 30 | ![image](https://user-images.githubusercontent.com/18059014/164895029-a2c066e5-38c4-4826-b1f7-4775b03ac05c.png) 31 | 32 | 33 | ## Usage with Storyboard 34 | In Identity Inspector, connect the UIView to SpringView Class and set the animation properties in Attribute Inspector. 35 | 36 | ![image](https://user-images.githubusercontent.com/18059014/164895509-feb27e48-23ad-41bf-b435-67fde2c3c2f8.png) 37 | 38 | ## Usage with Code 39 | springView.animation = "squeezeDown" 40 | springView.animate() 41 | 42 | ## Chaining Animations 43 | springView.y = -50 44 | animateToNext { 45 | springView.animation = "fall" 46 | springView.animateTo() 47 | } 48 | 49 | ## Functions 50 | animate() 51 | animateNext { ... } 52 | animateTo() 53 | animateToNext { ... } 54 | 55 | ## Animations 56 | pop 57 | slideLeft 58 | slideRight 59 | slideDown 60 | slideUp 61 | squeezeLeft 62 | squeezeRight 63 | squeezeDown 64 | squeezeUp 65 | fadeIn 66 | fadeOut 67 | fadeOutIn 68 | fadeInLeft 69 | fadeInRight 70 | fadeInDown 71 | fadeInUp 72 | zoomIn 73 | zoomOut 74 | fall 75 | shake 76 | flipX 77 | flipY 78 | morph 79 | squeeze 80 | flash 81 | wobble 82 | swing 83 | 84 | ## Curves 85 | easeIn 86 | easeOut 87 | easeInOut 88 | linear 89 | spring 90 | easeInSine 91 | easeOutSine 92 | easeInOutSine 93 | easeInQuad 94 | easeOutQuad 95 | easeInOutQuad 96 | easeInCubic 97 | easeOutCubic 98 | easeInOutCubic 99 | easeInQuart 100 | easeOutQuart 101 | easeInOutQuart 102 | easeInQuint 103 | easeOutQuint 104 | easeInOutQuint 105 | easeInExpo 106 | easeOutExpo 107 | easeInOutExpo 108 | easeInCirc 109 | easeOutCirc 110 | easeInOutCirc 111 | easeInBack 112 | easeOutBack 113 | easeInOutBack 114 | 115 | ## Properties 116 | autostart 117 | autohide 118 | 119 | title 120 | curve 121 | 122 | force 123 | delay 124 | duration 125 | damping 126 | velocity 127 | repeatCount 128 | x 129 | y 130 | scaleX 131 | scaleY 132 | scale 133 | rotate 134 | 135 | ## Autostart 136 | Allows you to animate without code. Don't need to use this if you plan to start the animation in code. 137 | 138 | ## Autohide 139 | Saves you the hassle of adding a line `springView.alpha = 0` in viewDidLoad(). 140 | 141 | ## Known issue 142 | 143 | 144 | ## License 145 | 146 | -------------------------------------------------------------------------------- /Sources/SpringAnimation/AnimationPreset.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Alexey Efimov on 13.04.2022. 6 | // 7 | 8 | public enum AnimationPreset: String, CaseIterable { 9 | case pop 10 | case slideLeft 11 | case slideRight 12 | case slideDown 13 | case slideUp 14 | case squeezeLeft 15 | case squeezeRight 16 | case squeezeDown 17 | case squeezeUp 18 | case fadeIn 19 | case fadeOut 20 | case fadeOutIn 21 | case fadeInLeft 22 | case fadeInRight 23 | case fadeInDown 24 | case fadeInUp 25 | case zoomIn 26 | case zoomOut 27 | case fall 28 | case shake 29 | case flipX 30 | case flipY 31 | case morph 32 | case squeeze 33 | case flash 34 | case wobble 35 | case swing 36 | } 37 | 38 | public enum AnimationCurve: String, CaseIterable { 39 | case easeIn 40 | case easeOut 41 | case easeInOut 42 | case linear 43 | case spring 44 | case easeInSine 45 | case easeOutSine 46 | case easeInOutSine 47 | case easeInQuad 48 | case easeOutQuad 49 | case easeInOutQuad 50 | case easeInCubic 51 | case easeOutCubic 52 | case easeInOutCubic 53 | case easeInQuart 54 | case easeOutQuart 55 | case easeInOutQuart 56 | case easeInQuint 57 | case easeOutQuint 58 | case easeInOutQuint 59 | case easeInExpo 60 | case easeOutExpo 61 | case easeInOutExpo 62 | case easeInCirc 63 | case easeOutCirc 64 | case easeInOutCirc 65 | case easeInBack 66 | case easeOutBack 67 | case easeInOutBack 68 | } 69 | -------------------------------------------------------------------------------- /Sources/SpringAnimation/DesignableButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DesignableButton.swift 3 | // 4 | // 5 | // Created by Alexey Efimov on 18.04.2022. 6 | // 7 | 8 | import UIKit 9 | 10 | @IBDesignable public class DesignableButton: SpringButton { 11 | 12 | @IBInspectable public var borderColor = UIColor.clear { 13 | didSet { 14 | layer.borderColor = borderColor.cgColor 15 | } 16 | } 17 | 18 | @IBInspectable public var borderWidth: CGFloat = 0 { 19 | didSet { 20 | layer.borderWidth = borderWidth 21 | } 22 | } 23 | 24 | @IBInspectable public var cornerRadius: CGFloat = 0 { 25 | didSet { 26 | layer.cornerRadius = cornerRadius 27 | } 28 | } 29 | 30 | @IBInspectable public var shadowColor = UIColor.clear { 31 | didSet { 32 | layer.shadowColor = shadowColor.cgColor 33 | } 34 | } 35 | 36 | @IBInspectable public var shadowRadius: CGFloat = 0 { 37 | didSet { 38 | layer.shadowRadius = shadowRadius 39 | } 40 | } 41 | 42 | @IBInspectable public var shadowOpacity: CGFloat = 0 { 43 | didSet { 44 | layer.shadowOpacity = Float(shadowOpacity) 45 | } 46 | } 47 | 48 | @IBInspectable public var shadowOffsetY: CGFloat = 0 { 49 | didSet { 50 | layer.shadowOffset.height = shadowOffsetY 51 | } 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /Sources/SpringAnimation/Extension + UIColor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Alexey Efimov on 15.04.2022. 6 | // 7 | 8 | import UIKit 9 | 10 | public extension UIColor { 11 | convenience init(hex: String) { 12 | var red: CGFloat = 0.0 13 | var green: CGFloat = 0.0 14 | var blue: CGFloat = 0.0 15 | var alpha: CGFloat = 1.0 16 | var hex: String = hex 17 | 18 | if hex.hasPrefix("#") { 19 | let index = hex.index(hex.startIndex, offsetBy: 1) 20 | hex = String(hex[index...]) 21 | } 22 | 23 | let scanner = Scanner(string: hex) 24 | var hexValue: CUnsignedLongLong = 0 25 | if scanner.scanHexInt64(&hexValue) { 26 | switch (hex.count) { 27 | case 3: 28 | red = CGFloat((hexValue & 0xF00) >> 8) / 15.0 29 | green = CGFloat((hexValue & 0x0F0) >> 4) / 15.0 30 | blue = CGFloat(hexValue & 0x00F) / 15.0 31 | case 4: 32 | red = CGFloat((hexValue & 0xF000) >> 12) / 15.0 33 | green = CGFloat((hexValue & 0x0F00) >> 8) / 15.0 34 | blue = CGFloat((hexValue & 0x00F0) >> 4) / 15.0 35 | alpha = CGFloat(hexValue & 0x000F) / 15.0 36 | case 6: 37 | red = CGFloat((hexValue & 0xFF0000) >> 16) / 255.0 38 | green = CGFloat((hexValue & 0x00FF00) >> 8) / 255.0 39 | blue = CGFloat(hexValue & 0x0000FF) / 255.0 40 | case 8: 41 | red = CGFloat((hexValue & 0xFF000000) >> 24) / 255.0 42 | green = CGFloat((hexValue & 0x00FF0000) >> 16) / 255.0 43 | blue = CGFloat((hexValue & 0x0000FF00) >> 8) / 255.0 44 | alpha = CGFloat(hexValue & 0x000000FF) / 255.0 45 | default: 46 | print("Invalid RGB string, number of characters after '#' should be either 3, 4, 6 or 8", terminator: "") 47 | } 48 | } else { 49 | print("Scan hex error") 50 | } 51 | self.init(red:red, green:green, blue:blue, alpha:alpha) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/SpringAnimation/SpringAnimation.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | public final class SpringAnimation { 4 | private unowned var view: Springable 5 | private var shouldAnimateAfterActive = false 6 | private var shouldAnimateInLayoutSubviews = true 7 | 8 | init(view: Springable) { 9 | self.view = view 10 | NotificationCenter.default.addObserver( 11 | self, 12 | selector: #selector(didBecomeActiveNotification), 13 | name: UIApplication.didBecomeActiveNotification, 14 | object: nil 15 | ) 16 | } 17 | 18 | public func awakeFromNib() { 19 | if view.autohide { 20 | view.alpha = 0 21 | } 22 | } 23 | 24 | public func layoutSubviews() { 25 | if shouldAnimateInLayoutSubviews { 26 | shouldAnimateInLayoutSubviews = false 27 | if view.autostart { 28 | if UIApplication.shared.applicationState != .active { 29 | shouldAnimateAfterActive = true 30 | return 31 | } 32 | view.alpha = 0 33 | view.animate() 34 | } 35 | } 36 | } 37 | 38 | public func animate() { 39 | view.animateFrom = true 40 | animationPreset() 41 | setView() 42 | } 43 | 44 | public func animateNext(completion: @escaping() -> Void) { 45 | view.animateFrom = true 46 | animationPreset() 47 | setView { 48 | completion() 49 | } 50 | } 51 | 52 | public func animateTo() { 53 | view.animateFrom = false 54 | animationPreset() 55 | setView() 56 | } 57 | 58 | public func animateToNext(completion: @escaping () -> ()) { 59 | view.animateFrom = false 60 | animationPreset() 61 | setView { 62 | completion() 63 | } 64 | } 65 | 66 | @objc private func didBecomeActiveNotification(_ notification: NSNotification) { 67 | if shouldAnimateAfterActive { 68 | view.alpha = 0 69 | view.animate() 70 | shouldAnimateAfterActive = false 71 | } 72 | } 73 | 74 | private func animationPreset() { 75 | view.alpha = 0.99 76 | if let animation = AnimationPreset(rawValue: view.animation) { 77 | switch animation { 78 | case .slideLeft: 79 | view.x = 300 * view.force 80 | case .slideRight: 81 | view.x = -300 * view.force 82 | case .slideDown: 83 | view.y = -300 * view.force 84 | case .slideUp: 85 | view.y = 300 * view.force 86 | case .squeezeLeft: 87 | view.x = 300 88 | view.scaleX = 3 * view.force 89 | case .squeezeRight: 90 | view.x = -300 91 | view.scaleX = 3 * view.force 92 | case .squeezeDown: 93 | view.y = -300 94 | view.scaleY = 3 * view.force 95 | case .squeezeUp: 96 | view.y = 300 97 | view.scaleY = 3 * view.force 98 | case .fadeIn: 99 | view.opacity = 0 100 | case .fadeOut: 101 | view.animateFrom = false 102 | view.opacity = 0 103 | case .fadeOutIn: 104 | let animation = CABasicAnimation() 105 | animation.keyPath = "opacity" 106 | animation.fromValue = 1 107 | animation.toValue = 0 108 | animation.timingFunction = getTimingFunction(with: view.curve) 109 | animation.duration = CFTimeInterval(view.duration) 110 | animation.beginTime = CACurrentMediaTime() + CFTimeInterval(view.delay) 111 | animation.autoreverses = true 112 | view.layer.add(animation, forKey: "fade") 113 | case .fadeInLeft: 114 | view.opacity = 0 115 | view.x = 300 * view.force 116 | case .fadeInRight: 117 | view.x = -300 * view.force 118 | view.opacity = 0 119 | case .fadeInDown: 120 | view.y = -300 * view.force 121 | view.opacity = 0 122 | case .fadeInUp: 123 | view.y = 300 * view.force 124 | view.opacity = 0 125 | case .zoomIn: 126 | view.opacity = 0 127 | view.scaleX = 2 * view.force 128 | view.scaleY = 2 * view.force 129 | case .zoomOut: 130 | view.animateFrom = false 131 | view.opacity = 0 132 | view.scaleX = 2 * view.force 133 | view.scaleY = 2 * view.force 134 | case .fall: 135 | view.animateFrom = false 136 | view.rotate = 15 * (CGFloat.pi / 180) 137 | view.y = 600 * view.force 138 | case .shake: 139 | let animation = CAKeyframeAnimation() 140 | animation.keyPath = "position.x" 141 | animation.values = [0,3 * view.force, -30 * view.force, 30 * view.force, 0] 142 | animation.keyTimes = [0, 0.2, 0.4, 0.6, 0.8, 1] 143 | animation.timingFunction = getTimingFunction(with: view.curve) 144 | animation.duration = CFTimeInterval(view.duration) 145 | animation.isAdditive = true 146 | animation.repeatCount = view.repeatCount 147 | animation.beginTime = CACurrentMediaTime() + CFTimeInterval(view.delay) 148 | view.layer.add(animation, forKey: "shake") 149 | case .pop: 150 | let animation = CAKeyframeAnimation() 151 | animation.keyPath = "transform.scale" 152 | animation.values = [0, 0.2 * view.force, -0.2 * view.force, 0.2 * view.force, 0] 153 | animation.keyTimes = [0, 0.2, 0.4, 0.6, 0.8, 1] 154 | animation.timingFunction = getTimingFunction(with: view.curve) 155 | animation.duration = CFTimeInterval(view.duration) 156 | animation.isAdditive = true 157 | animation.repeatCount = view.repeatCount 158 | animation.beginTime = CACurrentMediaTime() + CFTimeInterval(view.delay) 159 | view.layer.add(animation, forKey: "pop") 160 | case .flipX: 161 | view.rotate = 0 162 | view.scaleX = 1 163 | view.scaleY = 1 164 | var perspective = CATransform3DIdentity 165 | perspective.m34 = -1.0 / view.layer.frame.size.width / 2 166 | 167 | let animation = CABasicAnimation() 168 | animation.keyPath = "transform" 169 | animation.fromValue = NSValue(caTransform3D: CATransform3DMakeRotation(0, 0, 0, 0)) 170 | animation.toValue = NSValue( 171 | caTransform3D: CATransform3DConcat( 172 | perspective, 173 | CATransform3DMakeRotation(CGFloat.pi, 0, 1, 0) 174 | ) 175 | ) 176 | animation.duration = CFTimeInterval(view.duration) 177 | animation.repeatCount = view.repeatCount 178 | animation.beginTime = CACurrentMediaTime() + CFTimeInterval(view.delay) 179 | animation.timingFunction = getTimingFunction(with: view.curve) 180 | view.layer.add(animation, forKey: "3d") 181 | case .flipY: 182 | var perspective = CATransform3DIdentity 183 | perspective.m34 = -1.0 / view.layer.frame.size.width / 2 184 | 185 | let animation = CABasicAnimation() 186 | animation.keyPath = "transform" 187 | animation.fromValue = NSValue(caTransform3D: CATransform3DMakeRotation(0, 0, 0, 0)) 188 | animation.toValue = NSValue( 189 | caTransform3D: CATransform3DConcat( 190 | perspective, 191 | CATransform3DMakeRotation(CGFloat.pi, 1, 0, 0) 192 | ) 193 | ) 194 | animation.duration = CFTimeInterval(view.duration) 195 | animation.repeatCount = view.repeatCount 196 | animation.beginTime = CACurrentMediaTime() + CFTimeInterval(view.delay) 197 | animation.timingFunction = getTimingFunction(with: view.curve) 198 | view.layer.add(animation, forKey: "3d") 199 | case .morph: 200 | let morphX = CAKeyframeAnimation() 201 | morphX.keyPath = "transform.scale.x" 202 | morphX.values = [1, 1.3 * view.force, 0.7, 1.3 * view.force, 1] 203 | morphX.keyTimes = [0, 0.2, 0.4, 0.6, 0.8, 1] 204 | morphX.timingFunction = getTimingFunction(with: view.curve) 205 | morphX.duration = CFTimeInterval(view.duration) 206 | morphX.repeatCount = view.repeatCount 207 | morphX.beginTime = CACurrentMediaTime() + CFTimeInterval(view.delay) 208 | view.layer.add(morphX, forKey: "morphX") 209 | 210 | let morphY = CAKeyframeAnimation() 211 | morphY.keyPath = "transform.scale.y" 212 | morphY.values = [1, 0.7, 1.3 * view.force, 0.7, 1] 213 | morphY.keyTimes = [0, 0.2, 0.4, 0.6, 0.8, 1] 214 | morphY.timingFunction = getTimingFunction(with: view.curve) 215 | morphY.duration = CFTimeInterval(view.duration) 216 | morphY.repeatCount = view.repeatCount 217 | morphY.beginTime = CACurrentMediaTime() + CFTimeInterval(view.delay) 218 | view.layer.add(morphY, forKey: "morphY") 219 | case .squeeze: 220 | let morphX = CAKeyframeAnimation() 221 | morphX.keyPath = "transform.scale.x" 222 | morphX.values = [1, 1.5 * view.force, 0.5, 1.5 * view.force, 1] 223 | morphX.keyTimes = [0, 0.2, 0.4, 0.6, 0.8, 1] 224 | morphX.timingFunction = getTimingFunction(with: view.curve) 225 | morphX.duration = CFTimeInterval(view.duration) 226 | morphX.repeatCount = view.repeatCount 227 | morphX.beginTime = CACurrentMediaTime() + CFTimeInterval(view.delay) 228 | view.layer.add(morphX, forKey: "morphX") 229 | 230 | let morphY = CAKeyframeAnimation() 231 | morphY.keyPath = "transform.scale.y" 232 | morphY.values = [1, 0.5, 1, 0.5, 1] 233 | morphY.keyTimes = [0, 0.2, 0.4, 0.6, 0.8, 1] 234 | morphY.timingFunction = getTimingFunction(with: view.curve) 235 | morphY.duration = CFTimeInterval(view.duration) 236 | morphY.repeatCount = view.repeatCount 237 | morphY.beginTime = CACurrentMediaTime() + CFTimeInterval(view.delay) 238 | view.layer.add(morphY, forKey: "morphY") 239 | case .flash: 240 | let animation = CABasicAnimation() 241 | animation.keyPath = "opacity" 242 | animation.fromValue = 1 243 | animation.toValue = 0 244 | animation.duration = CFTimeInterval(view.duration) 245 | animation.repeatCount = view.repeatCount * 2.0 246 | animation.autoreverses = true 247 | animation.beginTime = CACurrentMediaTime() + CFTimeInterval(view.delay) 248 | view.layer.add(animation, forKey: "flash") 249 | case .wobble: 250 | let animation = CAKeyframeAnimation() 251 | animation.keyPath = "transform.rotation" 252 | animation.values = [0, 0.3 * view.force, -0.3 * view.force, 0.3 * view.force, 0] 253 | animation.keyTimes = [0, 0.2, 0.4, 0.6, 0.8, 1] 254 | animation.duration = CFTimeInterval(view.duration) 255 | animation.isAdditive = true 256 | animation.beginTime = CACurrentMediaTime() + CFTimeInterval(view.delay) 257 | view.layer.add(animation, forKey: "wobble") 258 | 259 | let x = CAKeyframeAnimation() 260 | x.keyPath = "position.x" 261 | x.values = [0, 30 * view.force, -30 * view.force, 30 * view.force, 0] 262 | x.keyTimes = [0, 0.2, 0.4, 0.6, 0.8, 1] 263 | x.timingFunction = getTimingFunction(with: view.curve) 264 | x.duration = CFTimeInterval(view.duration) 265 | x.isAdditive = true 266 | x.repeatCount = view.repeatCount 267 | x.beginTime = CACurrentMediaTime() + CFTimeInterval(view.delay) 268 | view.layer.add(x, forKey: "x") 269 | case .swing: 270 | let animation = CAKeyframeAnimation() 271 | animation.keyPath = "transform.rotation" 272 | animation.values = [0, 0.3 * view.force, -0.3 * view.force, 0.3 * view.force, 0] 273 | animation.keyTimes = [0, 0.2, 0.4, 0.6, 0.8, 1] 274 | animation.duration = CFTimeInterval(view.duration) 275 | animation.isAdditive = true 276 | animation.repeatCount = view.repeatCount 277 | animation.beginTime = CACurrentMediaTime() + CFTimeInterval(view.delay) 278 | view.layer.add(animation, forKey: "swing") 279 | } 280 | } 281 | } 282 | 283 | private func setView(completion: (() -> Void)? = nil) { 284 | if view.animateFrom { 285 | let translate = CGAffineTransform(translationX: view.x, y: view.y) 286 | let scale = CGAffineTransform(scaleX: view.scaleX, y: view.scaleY) 287 | let rotate = CGAffineTransform(rotationAngle: view.rotate) 288 | let translateAndScale = translate.concatenating(scale) 289 | view.transform = rotate.concatenating(translateAndScale) 290 | 291 | view.alpha = view.opacity 292 | } 293 | 294 | UIView.animate( 295 | withDuration: TimeInterval(view.duration), 296 | delay: TimeInterval(view.delay), 297 | usingSpringWithDamping: view.damping, 298 | initialSpringVelocity: view.velocity, 299 | options: [ 300 | getAnimationOptions(with: view.curve), 301 | UIView.AnimationOptions.allowUserInteraction 302 | ], 303 | animations: { [weak self] in 304 | guard let self = self else { return } 305 | if self.view.animateFrom { 306 | self.view.transform = CGAffineTransform.identity 307 | self.view.alpha = 1 308 | } else { 309 | let translate = CGAffineTransform(translationX: self.view.x, y: self.view.y) 310 | let scale = CGAffineTransform(scaleX: self.view.scaleX, y: self.view.scaleY) 311 | let rotate = CGAffineTransform(rotationAngle: self.view.rotate) 312 | let translateAndScale = translate.concatenating(scale) 313 | self.view.transform = rotate.concatenating(translateAndScale) 314 | self.view.alpha = self.view.opacity 315 | } 316 | }, 317 | completion: { [weak self] _ in 318 | completion?() 319 | self?.resetAll() 320 | } 321 | ) 322 | 323 | } 324 | 325 | private func getTimingFunction(with curve: String) -> CAMediaTimingFunction { 326 | if let curve = AnimationCurve(rawValue: curve) { 327 | switch curve { 328 | case .easeIn: return CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeIn) 329 | case .easeOut: return CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut) 330 | case .easeInOut: return CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut) 331 | case .linear: return CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear) 332 | case .spring: return CAMediaTimingFunction(controlPoints: 0.5, 1.1 + Float(view.force / 3), 1, 1) 333 | case .easeInSine: return CAMediaTimingFunction(controlPoints: 0.47, 0, 0.745, 0.715) 334 | case .easeOutSine: return CAMediaTimingFunction(controlPoints: 0.39, 0.575, 0.565, 1) 335 | case .easeInOutSine: return CAMediaTimingFunction(controlPoints: 0.445, 0.05, 0.55, 0.95) 336 | case .easeInQuad: return CAMediaTimingFunction(controlPoints: 0.55, 0.085, 0.68, 0.53) 337 | case .easeOutQuad: return CAMediaTimingFunction(controlPoints: 0.25, 0.46, 0.45, 0.94) 338 | case .easeInOutQuad: return CAMediaTimingFunction(controlPoints: 0.455, 0.03, 0.515, 0.955) 339 | case .easeInCubic: return CAMediaTimingFunction(controlPoints: 0.55, 0.055, 0.675, 0.19) 340 | case .easeOutCubic: return CAMediaTimingFunction(controlPoints: 0.215, 0.61, 0.355, 1) 341 | case .easeInOutCubic: return CAMediaTimingFunction(controlPoints: 0.645, 0.045, 0.355, 1) 342 | case .easeInQuart: return CAMediaTimingFunction(controlPoints: 0.895, 0.03, 0.685, 0.22) 343 | case .easeOutQuart: return CAMediaTimingFunction(controlPoints: 0.165, 0.84, 0.44, 1) 344 | case .easeInOutQuart: return CAMediaTimingFunction(controlPoints: 0.77, 0, 0.175, 1) 345 | case .easeInQuint: return CAMediaTimingFunction(controlPoints: 0.755, 0.05, 0.855, 0.06) 346 | case .easeOutQuint: return CAMediaTimingFunction(controlPoints: 0.23, 1, 0.32, 1) 347 | case .easeInOutQuint: return CAMediaTimingFunction(controlPoints: 0.86, 0, 0.07, 1) 348 | case .easeInExpo: return CAMediaTimingFunction(controlPoints: 0.95, 0.05, 0.795, 0.035) 349 | case .easeOutExpo: return CAMediaTimingFunction(controlPoints: 0.19, 1, 0.22, 1) 350 | case .easeInOutExpo: return CAMediaTimingFunction(controlPoints: 1, 0, 0, 1) 351 | case .easeInCirc: return CAMediaTimingFunction(controlPoints: 0.6, 0.04, 0.98, 0.335) 352 | case .easeOutCirc: return CAMediaTimingFunction(controlPoints: 0.075, 0.82, 0.165, 1) 353 | case .easeInOutCirc: return CAMediaTimingFunction(controlPoints: 0.785, 0.135, 0.15, 0.86) 354 | case .easeInBack: return CAMediaTimingFunction(controlPoints: 0.6, -0.28, 0.735, 0.045) 355 | case .easeOutBack: return CAMediaTimingFunction(controlPoints: 0.175, 0.885, 0.32, 1.275) 356 | case .easeInOutBack: return CAMediaTimingFunction(controlPoints: 0.68, -0.55, 0.265, 1.55) 357 | } 358 | } 359 | return CAMediaTimingFunction(name: CAMediaTimingFunctionName.default) 360 | } 361 | 362 | private func getAnimationOptions(with curve: String) -> UIView.AnimationOptions { 363 | if let curve = AnimationCurve(rawValue: curve) { 364 | switch curve { 365 | case .easeIn: return UIView.AnimationOptions.curveEaseIn 366 | case .easeOut: return UIView.AnimationOptions.curveEaseOut 367 | case .easeInOut: return UIView.AnimationOptions() 368 | default: break 369 | } 370 | } 371 | return UIView.AnimationOptions.curveLinear 372 | } 373 | 374 | private func resetAll() { 375 | view.x = 0 376 | view.y = 0 377 | view.animation = "" 378 | view.opacity = 1 379 | view.scaleX = 1 380 | view.scaleY = 1 381 | view.rotate = 0 382 | view.damping = 0.7 383 | view.velocity = 0.7 384 | view.repeatCount = 1 385 | view.delay = 0 386 | view.duration = 0.7 387 | } 388 | 389 | deinit { 390 | NotificationCenter.default.removeObserver( 391 | self, 392 | name: UIApplication.didBecomeActiveNotification, 393 | object: nil 394 | ) 395 | } 396 | } 397 | -------------------------------------------------------------------------------- /Sources/SpringAnimation/SpringButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SpringButton.swift 3 | // 4 | // 5 | // Created by Alexey Efimov on 18.04.2022. 6 | // 7 | 8 | import UIKit 9 | 10 | open class SpringButton: UIButton, Springable { 11 | @IBInspectable public var autostart: Bool = false 12 | @IBInspectable public var autohide: Bool = false 13 | @IBInspectable public var animation: String = "" 14 | @IBInspectable public var force: CGFloat = 1 15 | @IBInspectable public var delay: CGFloat = 0 16 | @IBInspectable public var duration: CGFloat = 0.7 17 | @IBInspectable public var damping: CGFloat = 0.7 18 | @IBInspectable public var velocity: CGFloat = 0.7 19 | @IBInspectable public var repeatCount: Float = 1 20 | @IBInspectable public var x: CGFloat = 0 21 | @IBInspectable public var y: CGFloat = 0 22 | @IBInspectable public var scaleX: CGFloat = 1 23 | @IBInspectable public var scaleY: CGFloat = 1 24 | @IBInspectable public var rotate: CGFloat = 0 25 | @IBInspectable public var curve: String = "" 26 | public var opacity: CGFloat = 1 27 | public var animateFrom = false 28 | 29 | lazy private var springAnimation = SpringAnimation(view: self) 30 | 31 | open override func awakeFromNib() { 32 | super.awakeFromNib() 33 | springAnimation.awakeFromNib() 34 | } 35 | 36 | open override func layoutSubviews() { 37 | super.layoutSubviews() 38 | springAnimation.layoutSubviews() 39 | } 40 | 41 | public func animate() { 42 | springAnimation.animate() 43 | } 44 | 45 | public func animateNext(completion: @escaping() -> Void) { 46 | springAnimation.animateNext(completion: completion) 47 | } 48 | 49 | public func animateTo() { 50 | springAnimation.animateTo() 51 | } 52 | 53 | public func animateToNext(completion: @escaping () -> ()) { 54 | springAnimation.animateToNext(completion: completion) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Sources/SpringAnimation/SpringImageView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SpringImageView.swift 3 | // 4 | // 5 | // Created by Alexey Efimov on 18.04.2022. 6 | // 7 | 8 | import UIKit 9 | 10 | open class SpringImageView: UIImageView, Springable { 11 | @IBInspectable public var autostart: Bool = false 12 | @IBInspectable public var autohide: Bool = false 13 | @IBInspectable public var animation: String = "" 14 | @IBInspectable public var force: CGFloat = 1 15 | @IBInspectable public var delay: CGFloat = 0 16 | @IBInspectable public var duration: CGFloat = 0.7 17 | @IBInspectable public var damping: CGFloat = 0.7 18 | @IBInspectable public var velocity: CGFloat = 0.7 19 | @IBInspectable public var repeatCount: Float = 1 20 | @IBInspectable public var x: CGFloat = 0 21 | @IBInspectable public var y: CGFloat = 0 22 | @IBInspectable public var scaleX: CGFloat = 1 23 | @IBInspectable public var scaleY: CGFloat = 1 24 | @IBInspectable public var rotate: CGFloat = 0 25 | @IBInspectable public var curve: String = "" 26 | public var opacity: CGFloat = 1 27 | public var animateFrom = false 28 | 29 | lazy private var springAnimation = SpringAnimation(view: self) 30 | 31 | open override func awakeFromNib() { 32 | super.awakeFromNib() 33 | springAnimation.awakeFromNib() 34 | } 35 | 36 | open override func layoutSubviews() { 37 | super.layoutSubviews() 38 | springAnimation.layoutSubviews() 39 | } 40 | 41 | public func animate() { 42 | springAnimation.animate() 43 | } 44 | 45 | public func animateNext(completion: @escaping() -> Void) { 46 | springAnimation.animateNext(completion: completion) 47 | } 48 | 49 | public func animateTo() { 50 | springAnimation.animateTo() 51 | } 52 | 53 | public func animateToNext(completion: @escaping () -> ()) { 54 | springAnimation.animateToNext(completion: completion) 55 | } 56 | } 57 | 58 | 59 | -------------------------------------------------------------------------------- /Sources/SpringAnimation/SpringLabel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SpringLabel.swift 3 | // 4 | // 5 | // Created by Alexey Efimov on 18.04.2022. 6 | // 7 | 8 | import UIKit 9 | 10 | open class SpringLabel: UILabel, Springable { 11 | @IBInspectable public var autostart: Bool = false 12 | @IBInspectable public var autohide: Bool = false 13 | @IBInspectable public var animation: String = "" 14 | @IBInspectable public var force: CGFloat = 1 15 | @IBInspectable public var delay: CGFloat = 0 16 | @IBInspectable public var duration: CGFloat = 0.7 17 | @IBInspectable public var damping: CGFloat = 0.7 18 | @IBInspectable public var velocity: CGFloat = 0.7 19 | @IBInspectable public var repeatCount: Float = 1 20 | @IBInspectable public var x: CGFloat = 0 21 | @IBInspectable public var y: CGFloat = 0 22 | @IBInspectable public var scaleX: CGFloat = 1 23 | @IBInspectable public var scaleY: CGFloat = 1 24 | @IBInspectable public var rotate: CGFloat = 0 25 | @IBInspectable public var curve: String = "" 26 | public var opacity: CGFloat = 1 27 | public var animateFrom = false 28 | 29 | lazy private var springAnimation = SpringAnimation(view: self) 30 | 31 | open override func awakeFromNib() { 32 | super.awakeFromNib() 33 | springAnimation.awakeFromNib() 34 | } 35 | 36 | open override func layoutSubviews() { 37 | super.layoutSubviews() 38 | springAnimation.layoutSubviews() 39 | } 40 | 41 | public func animate() { 42 | springAnimation.animate() 43 | } 44 | 45 | public func animateNext(completion: @escaping() -> Void) { 46 | springAnimation.animateNext(completion: completion) 47 | } 48 | 49 | public func animateTo() { 50 | springAnimation.animateTo() 51 | } 52 | 53 | public func animateToNext(completion: @escaping () -> ()) { 54 | springAnimation.animateToNext(completion: completion) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Sources/SpringAnimation/SpringTextField.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SpringTextField.swift 3 | // 4 | // 5 | // Created by Alexey Efimov on 18.04.2022. 6 | // 7 | 8 | import UIKit 9 | 10 | open class SpringTextField: UITextField, Springable { 11 | @IBInspectable public var autostart: Bool = false 12 | @IBInspectable public var autohide: Bool = false 13 | @IBInspectable public var animation: String = "" 14 | @IBInspectable public var force: CGFloat = 1 15 | @IBInspectable public var delay: CGFloat = 0 16 | @IBInspectable public var duration: CGFloat = 0.7 17 | @IBInspectable public var damping: CGFloat = 0.7 18 | @IBInspectable public var velocity: CGFloat = 0.7 19 | @IBInspectable public var repeatCount: Float = 1 20 | @IBInspectable public var x: CGFloat = 0 21 | @IBInspectable public var y: CGFloat = 0 22 | @IBInspectable public var scaleX: CGFloat = 1 23 | @IBInspectable public var scaleY: CGFloat = 1 24 | @IBInspectable public var rotate: CGFloat = 0 25 | @IBInspectable public var curve: String = "" 26 | public var opacity: CGFloat = 1 27 | public var animateFrom = false 28 | 29 | lazy private var springAnimation = SpringAnimation(view: self) 30 | 31 | open override func awakeFromNib() { 32 | super.awakeFromNib() 33 | springAnimation.awakeFromNib() 34 | } 35 | 36 | open override func layoutSubviews() { 37 | super.layoutSubviews() 38 | springAnimation.layoutSubviews() 39 | } 40 | 41 | public func animate() { 42 | springAnimation.animate() 43 | } 44 | 45 | public func animateNext(completion: @escaping() -> Void) { 46 | springAnimation.animateNext(completion: completion) 47 | } 48 | 49 | public func animateTo() { 50 | springAnimation.animateTo() 51 | } 52 | 53 | public func animateToNext(completion: @escaping () -> ()) { 54 | springAnimation.animateToNext(completion: completion) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Sources/SpringAnimation/SpringTextView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SpringTextView.swift 3 | // 4 | // 5 | // Created by Alexey Efimov on 18.04.2022. 6 | // 7 | 8 | import UIKit 9 | 10 | open class SpringTextView: UITextView, Springable { 11 | @IBInspectable public var autostart: Bool = false 12 | @IBInspectable public var autohide: Bool = false 13 | @IBInspectable public var animation: String = "" 14 | @IBInspectable public var force: CGFloat = 1 15 | @IBInspectable public var delay: CGFloat = 0 16 | @IBInspectable public var duration: CGFloat = 0.7 17 | @IBInspectable public var damping: CGFloat = 0.7 18 | @IBInspectable public var velocity: CGFloat = 0.7 19 | @IBInspectable public var repeatCount: Float = 1 20 | @IBInspectable public var x: CGFloat = 0 21 | @IBInspectable public var y: CGFloat = 0 22 | @IBInspectable public var scaleX: CGFloat = 1 23 | @IBInspectable public var scaleY: CGFloat = 1 24 | @IBInspectable public var rotate: CGFloat = 0 25 | @IBInspectable public var curve: String = "" 26 | public var opacity: CGFloat = 1 27 | public var animateFrom = false 28 | 29 | lazy private var springAnimation = SpringAnimation(view: self) 30 | 31 | open override func awakeFromNib() { 32 | super.awakeFromNib() 33 | springAnimation.awakeFromNib() 34 | } 35 | 36 | open override func layoutSubviews() { 37 | super.layoutSubviews() 38 | springAnimation.layoutSubviews() 39 | } 40 | 41 | public func animate() { 42 | springAnimation.animate() 43 | } 44 | 45 | public func animateNext(completion: @escaping() -> Void) { 46 | springAnimation.animateNext(completion: completion) 47 | } 48 | 49 | public func animateTo() { 50 | springAnimation.animateTo() 51 | } 52 | 53 | public func animateToNext(completion: @escaping () -> ()) { 54 | springAnimation.animateToNext(completion: completion) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Sources/SpringAnimation/SpringView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SpringView.swift 3 | // 4 | // 5 | // Created by Alexey Efimov on 13.04.2022. 6 | // 7 | 8 | import UIKit 9 | 10 | open class SpringView: UIView, Springable { 11 | @IBInspectable public var autostart: Bool = false 12 | @IBInspectable public var autohide: Bool = false 13 | @IBInspectable public var animation: String = "" 14 | @IBInspectable public var force: CGFloat = 1 15 | @IBInspectable public var delay: CGFloat = 0 16 | @IBInspectable public var duration: CGFloat = 0.7 17 | @IBInspectable public var damping: CGFloat = 0.7 18 | @IBInspectable public var velocity: CGFloat = 0.7 19 | @IBInspectable public var repeatCount: Float = 1 20 | @IBInspectable public var x: CGFloat = 0 21 | @IBInspectable public var y: CGFloat = 0 22 | @IBInspectable public var scaleX: CGFloat = 1 23 | @IBInspectable public var scaleY: CGFloat = 1 24 | @IBInspectable public var rotate: CGFloat = 0 25 | @IBInspectable public var curve: String = "" 26 | public var opacity: CGFloat = 1 27 | public var animateFrom = false 28 | 29 | lazy private var springAnimation = SpringAnimation(view: self) 30 | 31 | open override func awakeFromNib() { 32 | super.awakeFromNib() 33 | springAnimation.awakeFromNib() 34 | } 35 | 36 | open override func layoutSubviews() { 37 | super.layoutSubviews() 38 | springAnimation.layoutSubviews() 39 | } 40 | 41 | public func animate() { 42 | springAnimation.animate() 43 | } 44 | 45 | public func animateNext(completion: @escaping() -> Void) { 46 | springAnimation.animateNext(completion: completion) 47 | } 48 | 49 | public func animateTo() { 50 | springAnimation.animateTo() 51 | } 52 | 53 | public func animateToNext(completion: @escaping () -> ()) { 54 | springAnimation.animateToNext(completion: completion) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Sources/SpringAnimation/Springable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Alexey Efimov on 22.04.2022. 6 | // 7 | 8 | import UIKit 9 | 10 | /// Animation options 11 | public protocol Springable: AnyObject { 12 | // Animation properties 13 | /// Automatic animation start 14 | var autostart: Bool { get set } 15 | /// Hides the view 16 | var autohide: Bool { get set } 17 | /// Animation name 18 | var animation: String { get set } 19 | /// The of animation 20 | var force: CGFloat { get set } 21 | /// The delay (in seconds) after which the animations begin. 22 | /// 23 | /// The default value of this property is 0. When the value is greater than 0, the start of any animations is delayed by the specified amount of time. 24 | var delay: CGFloat { get set } 25 | /// The total duration of the animations, measured in seconds. If you specify a negative value or 0, the changes are made without animating them. 26 | var duration: CGFloat { get set } 27 | /// Defines how the spring’s motion should be damped due to the forces of friction. 28 | var damping: CGFloat { get set } 29 | /// The initial velocity of the object attached to the spring. 30 | var velocity: CGFloat { get set } 31 | /// Determines the number of times the animation will repeat. 32 | var repeatCount: Float { get set } 33 | /// The x-coordinate of the point. 34 | var x: CGFloat { get set } 35 | /// The y-coordinate of the point. 36 | var y: CGFloat { get set } 37 | /// A value function scales by the input value along the x-axis. Animations referencing this value transform function must provide a single animation value. 38 | var scaleX: CGFloat { get set } 39 | /// A value function scales by the input value along the y-axis. Animations referencing this value function must provide a single animation value. 40 | var scaleY: CGFloat { get set } 41 | /// Object rotation 42 | var rotate: CGFloat { get set } 43 | ///The opacity of the receiver. Animatable. 44 | /// 45 | ///The value of this property must be in the range 0.0 (transparent) to 1.0 (opaque). Values outside that range are clamped to the minimum or maximum. The default value of this property is 1.0. 46 | var opacity: CGFloat { get set } 47 | var animateFrom: Bool { get set } 48 | /// Animation preset 49 | var curve: String { get set } 50 | 51 | // UIView 52 | var layer : CALayer { get } 53 | var transform : CGAffineTransform { get set } 54 | var alpha : CGFloat { get set } 55 | 56 | /// Run the animation with the given parameters 57 | func animate() 58 | /// Run next animation after complete current animation 59 | func animateNext(completion: @escaping() -> Void) 60 | 61 | func animateTo() 62 | 63 | func animateToNext(completion: @escaping () -> ()) 64 | } 65 | -------------------------------------------------------------------------------- /Tests/SpringAnimationTests/SpringAnimationTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import SpringAnimation 3 | 4 | final class SpringAnimationTests: 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(SpringAnimation().text, "Hello, World!") 10 | } 11 | } 12 | --------------------------------------------------------------------------------