├── SpringAnimation ├── README.md ├── .gitignore ├── Tests │ └── SpringAnimationTests │ │ └── SpringAnimationTests.swift ├── Package.swift └── Sources │ └── SpringAnimation │ ├── DesignableButton.swift │ ├── AnimationPreset.swift │ ├── SpringView.swift │ ├── SpringLabel.swift │ ├── SpringButton.swift │ ├── SpringTextField.swift │ ├── SpringTextView.swift │ ├── SpringImageView.swift │ ├── Extension + UIColor.swift │ ├── Springable.swift │ └── SpringAnimation.swift ├── SpringApp ├── Resources │ └── Assets.xcassets │ │ ├── Contents.json │ │ ├── icon-shape.imageset │ │ ├── icon-shape.pdf │ │ └── Contents.json │ │ ├── AccentColor.colorset │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ └── Contents.json ├── Code │ ├── CodeRouter.swift │ ├── CodeModels.swift │ ├── CodeInteractor.swift │ ├── CodePresenter.swift │ └── CodeViewController.swift ├── Options │ ├── OptionsRouter.swift │ ├── OptionsModels.swift │ ├── OptionsPresenter.swift │ ├── OptionsInteractor.swift │ └── OptionsViewController.swift ├── App │ ├── Info.plist │ ├── AppDelegate.swift │ └── SceneDelegate.swift ├── Models │ └── Animation.swift ├── StoryBoards │ └── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard └── Spring │ ├── SpringPresenter.swift │ ├── SpringRouter.swift │ ├── SpringInteractor.swift │ ├── SpringModels.swift │ └── SpringViewController.swift ├── README.md └── SpringApp.xcodeproj ├── project.xcworkspace └── contents.xcworkspacedata ├── xcuserdata └── debash.xcuserdatad │ └── xcdebugger │ └── Breakpoints_v2.xcbkptlist └── project.pbxproj /SpringAnimation/README.md: -------------------------------------------------------------------------------- 1 | # SpringAnimation 2 | 3 | A description of this package. 4 | -------------------------------------------------------------------------------- /SpringApp/Resources/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SpringApp 2 | 3 | This is an application created to demonstrate the capabilities of the SpringAnimation framework: https://github.com/LexDeBash/SpringAnimation 4 | -------------------------------------------------------------------------------- /SpringApp/Resources/Assets.xcassets/icon-shape.imageset/icon-shape.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LexDeBash/SpringApp/HEAD/SpringApp/Resources/Assets.xcassets/icon-shape.imageset/icon-shape.pdf -------------------------------------------------------------------------------- /SpringApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /SpringAnimation/.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 | -------------------------------------------------------------------------------- /SpringApp.xcodeproj/xcuserdata/debash.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /SpringApp/Resources/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /SpringApp/Resources/Assets.xcassets/icon-shape.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "icon-shape.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /SpringAnimation/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 | -------------------------------------------------------------------------------- /SpringApp/Code/CodeRouter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CodeRouter.swift 3 | // SpringApp 4 | // 5 | // Created by Alexey Efimov on 18.04.2022. 6 | // Copyright (c) 2022 ___ORGANIZATIONNAME___. All rights reserved. 7 | // 8 | // This file was generated by the Clean Swift Xcode Templates so 9 | // you can apply clean architecture to your iOS and Mac projects, 10 | // see http://clean-swift.com 11 | // 12 | 13 | import UIKit 14 | 15 | @objc protocol CodeRoutingLogic { 16 | 17 | } 18 | 19 | protocol CodeDataPassing { 20 | var dataStore: CodeDataStore? { get } 21 | } 22 | 23 | class CodeRouter: NSObject, CodeRoutingLogic, CodeDataPassing { 24 | weak var viewController: CodeViewController? 25 | var dataStore: CodeDataStore? 26 | } 27 | -------------------------------------------------------------------------------- /SpringApp/Options/OptionsRouter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OptionsRouter.swift 3 | // SpringApp 4 | // 5 | // Created by Alexey Efimov on 17.04.2022. 6 | // Copyright (c) 2022 ___ORGANIZATIONNAME___. All rights reserved. 7 | // 8 | // This file was generated by the Clean Swift Xcode Templates so 9 | // you can apply clean architecture to your iOS and Mac projects, 10 | // see http://clean-swift.com 11 | // 12 | 13 | import UIKit 14 | 15 | @objc protocol OptionsRoutingLogic { 16 | 17 | } 18 | 19 | protocol OptionsDataPassing { 20 | var dataStore: OptionsDataStore? { get } 21 | } 22 | 23 | class OptionsRouter: NSObject, OptionsRoutingLogic, OptionsDataPassing { 24 | 25 | weak var viewController: OptionsViewController? 26 | var dataStore: OptionsDataStore? 27 | } 28 | -------------------------------------------------------------------------------- /SpringApp/Code/CodeModels.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CodeModels.swift 3 | // SpringApp 4 | // 5 | // Created by Alexey Efimov on 18.04.2022. 6 | // Copyright (c) 2022 ___ORGANIZATIONNAME___. All rights reserved. 7 | // 8 | // This file was generated by the Clean Swift Xcode Templates so 9 | // you can apply clean architecture to your iOS and Mac projects, 10 | // see http://clean-swift.com 11 | // 12 | 13 | typealias CodeResponse = Code.PresentCode.Response 14 | typealias CodeViewModel = Code.PresentCode.ViewModel 15 | 16 | enum Code { 17 | 18 | // MARK: Use cases 19 | enum PresentCode { 20 | 21 | struct Response { 22 | let animation: Animation 23 | } 24 | 25 | struct ViewModel { 26 | let codeText: String 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /SpringApp/App/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UIApplicationSceneManifest 6 | 7 | UIApplicationSupportsMultipleScenes 8 | 9 | UISceneConfigurations 10 | 11 | UIWindowSceneSessionRoleApplication 12 | 13 | 14 | UISceneConfigurationName 15 | Default Configuration 16 | UISceneDelegateClassName 17 | $(PRODUCT_MODULE_NAME).SceneDelegate 18 | UISceneStoryboardFile 19 | Main 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /SpringApp/Code/CodeInteractor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CodeInteractor.swift 3 | // SpringApp 4 | // 5 | // Created by Alexey Efimov on 18.04.2022. 6 | // Copyright (c) 2022 ___ORGANIZATIONNAME___. All rights reserved. 7 | // 8 | // This file was generated by the Clean Swift Xcode Templates so 9 | // you can apply clean architecture to your iOS and Mac projects, 10 | // see http://clean-swift.com 11 | // 12 | 13 | protocol CodeBusinessLogic { 14 | func showCode() 15 | } 16 | 17 | protocol CodeDataStore { 18 | var animation: Animation? { get set } 19 | } 20 | 21 | class CodeInteractor: CodeBusinessLogic, CodeDataStore { 22 | 23 | var presenter: CodePresentationLogic? 24 | var animation: Animation? 25 | 26 | func showCode() { 27 | guard let animation = animation else { return } 28 | let response = CodeResponse(animation: animation) 29 | presenter?.presentCode(response: response) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /SpringAnimation/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 | -------------------------------------------------------------------------------- /SpringApp/Models/Animation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Animation.swift 3 | // SpringApp 4 | // 5 | // Created by Alexey Efimov on 16.04.2022. 6 | // 7 | 8 | import Foundation 9 | 10 | struct Animation { 11 | var autostart: Bool 12 | var autohide: Bool 13 | 14 | var title: String 15 | var curve: String 16 | 17 | var force: Double 18 | var delay: Double 19 | var duration: Double 20 | var damping: Double 21 | var velocity: Double 22 | var repeatCount: Float 23 | var x: Double 24 | var y: Double 25 | var scaleX: Double 26 | var scaleY: Double 27 | var scale: Double 28 | var rotate: Double 29 | 30 | static func getDefaultValues() -> Animation { 31 | Animation( 32 | autostart: false, 33 | autohide: false, 34 | title: "pop", 35 | curve: "easeInt", 36 | force: 1, 37 | delay: 0, 38 | duration: 0.7, 39 | damping: 0.7, 40 | velocity: 0.7, 41 | repeatCount: 1, 42 | x: 1, 43 | y: 1, 44 | scaleX: 1, 45 | scaleY: 1, 46 | scale: 1, 47 | rotate: 0 48 | ) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /SpringAnimation/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 | -------------------------------------------------------------------------------- /SpringApp/App/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // SpringApp 4 | // 5 | // Created by Alexey Efimov on 16.04.2022. 6 | // 7 | 8 | import UIKit 9 | 10 | @main 11 | class AppDelegate: UIResponder, UIApplicationDelegate { 12 | 13 | 14 | 15 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 16 | // Override point for customization after application launch. 17 | return true 18 | } 19 | 20 | // MARK: UISceneSession Lifecycle 21 | 22 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 23 | // Called when a new scene session is being created. 24 | // Use this method to select a configuration to create the new scene with. 25 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 26 | } 27 | 28 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 29 | // Called when the user discards a scene session. 30 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 31 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 32 | } 33 | 34 | 35 | } 36 | 37 | -------------------------------------------------------------------------------- /SpringAnimation/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 | -------------------------------------------------------------------------------- /SpringApp/StoryBoards/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /SpringApp/Spring/SpringPresenter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SpringPresenter.swift 3 | // SpringApp 4 | // 5 | // Created by Alexey Efimov on 16.04.2022. 6 | // Copyright (c) 2022 ___ORGANIZATIONNAME___. All rights reserved. 7 | // 8 | // This file was generated by the Clean Swift Xcode Templates so 9 | // you can apply clean architecture to your iOS and Mac projects, 10 | // see http://clean-swift.com 11 | // 12 | 13 | import QuartzCore 14 | 15 | protocol SpringPresentationLogic { 16 | func presentInitialValues(response: SpringResponse) 17 | func presentAnimation(response: SpringResponse) 18 | func presentTransformation(response: TransformResponse) 19 | } 20 | 21 | class SpringPresenter: SpringPresentationLogic { 22 | 23 | weak var viewController: SpringDisplayLogic? 24 | 25 | func presentInitialValues(response: SpringResponse) { 26 | var viewModel = SpringViewModel(animation: response.animation) 27 | viewModel.animationList = response.animationList 28 | viewModel.curveList = response.curveList 29 | viewController?.displayInitialValues(viewModel: viewModel) 30 | } 31 | 32 | func presentAnimation(response: SpringResponse) { 33 | let viewModel = SpringViewModel(animation: response.animation) 34 | viewController?.displayAnimation(viewModel: viewModel) 35 | } 36 | 37 | func presentTransformation(response: TransformResponse) { 38 | let animation = CABasicAnimation() 39 | animation.keyPath = response.keyPath 40 | animation.fromValue = response.fromValue 41 | animation.toValue = response.toValue 42 | animation.duration = response.duration 43 | let viewModel = TransformViewModel( 44 | cornerRadius: response.toValue, 45 | animation: animation, 46 | key: "radius" 47 | ) 48 | viewController?.displayTransformation(viewModel: viewModel) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /SpringAnimation/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.customAwakeFromNib() 34 | } 35 | 36 | open override func layoutSubviews() { 37 | super.layoutSubviews() 38 | springAnimation.customLayoutSubviews() 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 | -------------------------------------------------------------------------------- /SpringAnimation/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.customAwakeFromNib() 34 | } 35 | 36 | open override func layoutSubviews() { 37 | super.layoutSubviews() 38 | springAnimation.customLayoutSubviews() 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 | -------------------------------------------------------------------------------- /SpringAnimation/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.customAwakeFromNib() 34 | } 35 | 36 | open override func layoutSubviews() { 37 | super.layoutSubviews() 38 | springAnimation.customLayoutSubviews() 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 | -------------------------------------------------------------------------------- /SpringAnimation/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.customAwakeFromNib() 34 | } 35 | 36 | open override func layoutSubviews() { 37 | super.layoutSubviews() 38 | springAnimation.customLayoutSubviews() 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 | -------------------------------------------------------------------------------- /SpringAnimation/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.customAwakeFromNib() 34 | } 35 | 36 | open override func layoutSubviews() { 37 | super.layoutSubviews() 38 | springAnimation.customLayoutSubviews() 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 | -------------------------------------------------------------------------------- /SpringAnimation/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.customAwakeFromNib() 34 | } 35 | 36 | open override func layoutSubviews() { 37 | super.layoutSubviews() 38 | springAnimation.customLayoutSubviews() 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 | -------------------------------------------------------------------------------- /SpringApp/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /SpringApp/Options/OptionsModels.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OptionsModels.swift 3 | // SpringApp 4 | // 5 | // Created by Alexey Efimov on 17.04.2022. 6 | // Copyright (c) 2022 ___ORGANIZATIONNAME___. All rights reserved. 7 | // 8 | // This file was generated by the Clean Swift Xcode Templates so 9 | // you can apply clean architecture to your iOS and Mac projects, 10 | // see http://clean-swift.com 11 | // 12 | 13 | typealias OptionsRequest = Options.Animate.Request 14 | typealias OptionsResponse = Options.Animate.Response 15 | typealias OptionsViewModel = Options.Animate.ViewModel 16 | 17 | enum Options { 18 | 19 | // MARK: Use cases 20 | enum Animate { 21 | struct Request { 22 | var rowIndex = 0 23 | var forceSliderValue: Float = 1 24 | var durationSliderValue: Float = 1 25 | var delaySliderValue: Float = 0 26 | var damping: Float = 0.7 27 | var velocity: Float = 0.7 28 | var rotate: Float = 0 29 | var x: Float = 0 30 | var y: Float = 0 31 | var scale: Float = 0 32 | } 33 | 34 | struct Response { 35 | let animation: Animation 36 | } 37 | 38 | struct ViewModel { 39 | let autostart: Bool 40 | let autohide: Bool 41 | 42 | let title: String 43 | let curve: String 44 | 45 | let force: Double 46 | let delay: Double 47 | let duration: Double 48 | let damping: Double 49 | let velocity: Double 50 | let repeatCount: Float 51 | let x: Double 52 | let y: Double 53 | let scaleX: Double 54 | let scaleY: Double 55 | let scale: Double 56 | let rotate: Double 57 | 58 | let forceText: String 59 | let durationText: String 60 | let delayText: String 61 | let dampingText: String 62 | let velocityText: String 63 | let scaleText: String 64 | let xText: String 65 | let yText: String 66 | let rotateText: String 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /SpringAnimation/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 | -------------------------------------------------------------------------------- /SpringApp/Options/OptionsPresenter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OptionsPresenter.swift 3 | // SpringApp 4 | // 5 | // Created by Alexey Efimov on 17.04.2022. 6 | // Copyright (c) 2022 ___ORGANIZATIONNAME___. All rights reserved. 7 | // 8 | // This file was generated by the Clean Swift Xcode Templates so 9 | // you can apply clean architecture to your iOS and Mac projects, 10 | // see http://clean-swift.com 11 | // 12 | 13 | protocol OptionsPresentationLogic { 14 | func presentOptions(response: OptionsResponse) 15 | } 16 | 17 | class OptionsPresenter: OptionsPresentationLogic { 18 | 19 | weak var viewController: OptionsDisplayLogic? 20 | 21 | func presentOptions(response: OptionsResponse) { 22 | let viewModel = OptionsViewModel( 23 | autostart: response.animation.autostart, 24 | autohide: response.animation.autohide, 25 | title: response.animation.title, 26 | curve: response.animation.curve, 27 | force: response.animation.force, 28 | delay: response.animation.delay, 29 | duration: response.animation.duration, 30 | damping: response.animation.damping, 31 | velocity: response.animation.velocity, 32 | repeatCount: response.animation.repeatCount, 33 | x: response.animation.x, 34 | y: response.animation.y, 35 | scaleX: response.animation.scaleX, 36 | scaleY: response.animation.scaleY, 37 | scale: response.animation.scale, 38 | rotate: response.animation.rotate, 39 | forceText: String(format: "Force: %.1f", response.animation.force), 40 | durationText: String(format: "Duration: %.1f", response.animation.duration), 41 | delayText: String(format: "Delay: %.1f", response.animation.delay), 42 | dampingText: String(format: "Damping: %.1f", response.animation.damping), 43 | velocityText: String(format: "Velocity: %.1f", response.animation.velocity), 44 | scaleText: String(format: "Scale: %.1f", response.animation.scale), 45 | xText: String(format: "x: %.1f", response.animation.x), 46 | yText: String(format: "y: %.1f", response.animation.y), 47 | rotateText: String(format: "Rotate: %.1f", response.animation.rotate) 48 | ) 49 | viewController?.displayOptions(viewModel: viewModel) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /SpringApp/App/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // SpringApp 4 | // 5 | // Created by Alexey Efimov on 16.04.2022. 6 | // 7 | 8 | import UIKit 9 | 10 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 11 | 12 | var window: UIWindow? 13 | 14 | 15 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 16 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 17 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 18 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 19 | guard let _ = (scene as? UIWindowScene) else { return } 20 | } 21 | 22 | func sceneDidDisconnect(_ scene: UIScene) { 23 | // Called as the scene is being released by the system. 24 | // This occurs shortly after the scene enters the background, or when its session is discarded. 25 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 26 | // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). 27 | } 28 | 29 | func sceneDidBecomeActive(_ scene: UIScene) { 30 | // Called when the scene has moved from an inactive state to an active state. 31 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 32 | } 33 | 34 | func sceneWillResignActive(_ scene: UIScene) { 35 | // Called when the scene will move from an active state to an inactive state. 36 | // This may occur due to temporary interruptions (ex. an incoming phone call). 37 | } 38 | 39 | func sceneWillEnterForeground(_ scene: UIScene) { 40 | // Called as the scene transitions from the background to the foreground. 41 | // Use this method to undo the changes made on entering the background. 42 | } 43 | 44 | func sceneDidEnterBackground(_ scene: UIScene) { 45 | // Called as the scene transitions from the foreground to the background. 46 | // Use this method to save data, release shared resources, and store enough scene-specific state information 47 | // to restore the scene back to its current state. 48 | } 49 | 50 | 51 | } 52 | 53 | -------------------------------------------------------------------------------- /SpringApp/Spring/SpringRouter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SpringRouter.swift 3 | // SpringApp 4 | // 5 | // Created by Alexey Efimov on 16.04.2022. 6 | // Copyright (c) 2022 ___ORGANIZATIONNAME___. All rights reserved. 7 | // 8 | // This file was generated by the Clean Swift Xcode Templates so 9 | // you can apply clean architecture to your iOS and Mac projects, 10 | // see http://clean-swift.com 11 | // 12 | 13 | import UIKit 14 | 15 | @objc protocol SpringRoutingLogic { 16 | func routeToOptions(segue: UIStoryboardSegue?) 17 | func routeToCode() 18 | } 19 | 20 | protocol SpringDataPassing { 21 | var dataStore: SpringDataStore? { get } 22 | } 23 | 24 | class SpringRouter: NSObject, SpringRoutingLogic, SpringDataPassing { 25 | 26 | weak var springVC: SpringViewController? 27 | var dataStore: SpringDataStore? 28 | 29 | // MARK: Routing 30 | func routeToOptions(segue: UIStoryboardSegue?) { 31 | if let segue = segue { 32 | let destinationVC = segue.destination as! OptionsViewController 33 | var destinationDS = destinationVC.router!.dataStore! 34 | passDataToOptions(source: dataStore!, destination: &destinationDS) 35 | destinationVC.delegate = springVC 36 | destinationDS.delegate = dataStore 37 | } 38 | } 39 | 40 | func routeToCode() { 41 | let destinationVC = CodeViewController() 42 | var destinationDS = destinationVC.router!.dataStore! 43 | passDataToCode(source: dataStore!, destination: &destinationDS) 44 | navigateToCode(source: springVC!, destination: destinationVC) 45 | } 46 | 47 | // MARK: Navigation 48 | private func navigateToOptions(source: SpringViewController, destination: OptionsViewController) { 49 | source.present(destination, animated: true) 50 | } 51 | 52 | private func navigateToCode(source: SpringViewController, destination: CodeViewController) { 53 | guard let sheetView = destination.sheetPresentationController else { return } 54 | sheetView.detents = [.medium()] 55 | source.present(destination, animated: false) 56 | } 57 | 58 | // MARK: Passing data 59 | private func passDataToOptions(source: SpringDataStore, destination: inout OptionsDataStore) { 60 | destination.animation = source.animation 61 | } 62 | 63 | private func passDataToCode(source: SpringDataStore, destination: inout CodeDataStore) { 64 | destination.animation = source.animation 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /SpringApp/Code/CodePresenter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CodePresenter.swift 3 | // SpringApp 4 | // 5 | // Created by Alexey Efimov on 18.04.2022. 6 | // Copyright (c) 2022 ___ORGANIZATIONNAME___. All rights reserved. 7 | // 8 | // This file was generated by the Clean Swift Xcode Templates so 9 | // you can apply clean architecture to your iOS and Mac projects, 10 | // see http://clean-swift.com 11 | // 12 | 13 | protocol CodePresentationLogic { 14 | func presentCode(response: CodeResponse) 15 | } 16 | 17 | class CodePresenter: CodePresentationLogic { 18 | 19 | weak var viewController: CodeDisplayLogic? 20 | 21 | func presentCode(response: CodeResponse) { 22 | var codeText = "" 23 | 24 | if !response.animation.title.isEmpty { 25 | codeText += " view.animation = \"\(response.animation.title)\"\n" 26 | } 27 | if !response.animation.curve.isEmpty { 28 | codeText += " view.curve = \"\(response.animation.curve)\"\n" 29 | } 30 | if response.animation.force != 1 { 31 | codeText += String(format: "\n view.force = %.1f\n", response.animation.force) 32 | } 33 | if response.animation.duration != 0.7 { 34 | codeText += String(format: " view.duration = %.1f\n", response.animation.duration) 35 | } 36 | if response.animation.delay != 0 { 37 | codeText += String(format: " view.delay = %.1f\n", response.animation.delay) 38 | } 39 | if response.animation.damping != 0.7 { 40 | codeText += String(format: " view.damping = %.1f\n", response.animation.damping) 41 | } 42 | if response.animation.velocity != 0.7 { 43 | codeText += String(format: " view.velocity = %.1f\n", response.animation.velocity) 44 | } 45 | if response.animation.rotate != 0 { 46 | codeText += String(format: " view.rotate = %.1f\n", response.animation.rotate) 47 | } 48 | if response.animation.x != 0 { 49 | codeText += String(format: " view.x = %.1f\n", response.animation.x) 50 | } 51 | if response.animation.y != 0 { 52 | codeText += String(format: " view.y = %.1f\n", response.animation.y) 53 | } 54 | if response.animation.scale != 1 { 55 | codeText += String(format: " view.scale = %.1f\n", response.animation.scale) 56 | } 57 | codeText += "\n view.animate()" 58 | let viewModel = CodeViewModel(codeText: codeText) 59 | viewController?.displayCode(viewModel: viewModel) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /SpringApp/Options/OptionsInteractor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OptionsInteractor.swift 3 | // SpringApp 4 | // 5 | // Created by Alexey Efimov on 17.04.2022. 6 | // Copyright (c) 2022 ___ORGANIZATIONNAME___. All rights reserved. 7 | // 8 | // This file was generated by the Clean Swift Xcode Templates so 9 | // you can apply clean architecture to your iOS and Mac projects, 10 | // see http://clean-swift.com 11 | // 12 | 13 | protocol OptionsBusinessLogic { 14 | func setOptions() 15 | func dampingSliderDidChanged(request: OptionsRequest) 16 | func velocitySliderDidChanged(request: OptionsRequest) 17 | func scaleSliderDidChanged(request: OptionsRequest) 18 | func xSliderDidChanged(request: OptionsRequest) 19 | func ySliderDidChanged(request: OptionsRequest) 20 | func rotateSliderDidChanged(request: OptionsRequest) 21 | func resetButtonPressed() 22 | } 23 | 24 | protocol OptionsDataStore { 25 | var animation: Animation? { get set } 26 | var delegate: SpringDataStore? { get set } 27 | } 28 | 29 | class OptionsInteractor: OptionsBusinessLogic, OptionsDataStore { 30 | 31 | var presenter: OptionsPresentationLogic? 32 | var animation: Animation? 33 | var delegate: SpringDataStore? 34 | 35 | private var response: OptionsResponse { 36 | OptionsResponse(animation: animation ?? Animation.getDefaultValues()) 37 | } 38 | 39 | func setOptions() { 40 | presenter?.presentOptions(response: response) 41 | passDataToPresenter() 42 | } 43 | 44 | func dampingSliderDidChanged(request: OptionsRequest) { 45 | animation?.damping = Double(request.damping) 46 | passDataToPresenter() 47 | } 48 | 49 | func velocitySliderDidChanged(request: OptionsRequest) { 50 | animation?.velocity = Double(request.velocity) 51 | passDataToPresenter() 52 | } 53 | 54 | func scaleSliderDidChanged(request: OptionsRequest) { 55 | animation?.scale = Double(request.scale) 56 | passDataToPresenter() 57 | } 58 | 59 | func xSliderDidChanged(request: OptionsRequest) { 60 | animation?.x = Double(request.x) 61 | passDataToPresenter() 62 | } 63 | 64 | func ySliderDidChanged(request: OptionsRequest) { 65 | animation?.y = Double(request.y) 66 | passDataToPresenter() 67 | } 68 | 69 | func rotateSliderDidChanged(request: OptionsRequest) { 70 | animation?.rotate = Double(request.rotate) 71 | passDataToPresenter() 72 | } 73 | 74 | func resetButtonPressed() { 75 | animation = Animation.getDefaultValues() 76 | passDataToPresenter() 77 | } 78 | 79 | private func passDataToPresenter() { 80 | delegate?.animation = animation ?? Animation.getDefaultValues() 81 | presenter?.presentOptions(response: response) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /SpringAnimation/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 | -------------------------------------------------------------------------------- /SpringApp/Spring/SpringInteractor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SpringInteractor.swift 3 | // SpringApp 4 | // 5 | // Created by Alexey Efimov on 16.04.2022. 6 | // Copyright (c) 2022 ___ORGANIZATIONNAME___. All rights reserved. 7 | // 8 | // This file was generated by the Clean Swift Xcode Templates so 9 | // you can apply clean architecture to your iOS and Mac projects, 10 | // see http://clean-swift.com 11 | // 12 | 13 | import SpringAnimation 14 | 15 | protocol SpringBusinessLogic { 16 | func setInitialValues(request: SpringRequest) 17 | func didTapView() 18 | func setViewSize(request: TransformRequest) 19 | func didSelectAnimationRow(request: SpringRequest) 20 | func didSelectCurveRow(request: SpringRequest) 21 | func forceSliderDidChanged(request: SpringRequest) 22 | func durationSliderDidChanged(request: SpringRequest) 23 | func delaySliderDidChanged(request: SpringRequest) 24 | func transformSpringViewButtonDidTapped() 25 | } 26 | 27 | protocol SpringDataStore { 28 | var animation: Animation { get set } 29 | var animationList: [String] { get } 30 | var curveList: [String] { get } 31 | var viewHeight: Double { get } 32 | var isCircle: Bool { get } 33 | } 34 | 35 | class SpringInteractor: SpringBusinessLogic, SpringDataStore { 36 | 37 | var presenter: SpringPresentationLogic? 38 | var animation = Animation.getDefaultValues() 39 | var animationList = AnimationPreset.allCases.map { $0.rawValue } 40 | var curveList = AnimationCurve.allCases.map { $0.rawValue } 41 | var viewHeight: Double = 0 42 | var isCircle = false 43 | 44 | private var response: SpringResponse { 45 | SpringResponse(animation: animation) 46 | } 47 | 48 | func setInitialValues(request: SpringRequest) { 49 | animation = Animation( 50 | autostart: request.autostart, 51 | autohide: request.autohide, 52 | title: request.title, 53 | curve: request.curve, 54 | force: request.force, 55 | delay: request.delay, 56 | duration: request.duration, 57 | damping: request.damping, 58 | velocity: request.velocity, 59 | repeatCount: request.repeatCount, 60 | x: request.x, 61 | y: request.y, 62 | scaleX: request.scaleX, 63 | scaleY: request.scaleY, 64 | scale: 1, 65 | rotate: request.rotate 66 | ) 67 | var response = SpringResponse(animation: animation) 68 | response.animationList = animationList 69 | response.curveList = curveList 70 | presenter?.presentInitialValues(response: response) 71 | } 72 | 73 | func setViewSize(request: TransformRequest) { 74 | viewHeight = request.viewSize 75 | } 76 | 77 | func didTapView() { 78 | presenter?.presentAnimation(response: response) 79 | } 80 | 81 | func didSelectAnimationRow(request: SpringRequest) { 82 | animation.title = animationList[request.rowIndex] 83 | presenter?.presentAnimation(response: response) 84 | } 85 | 86 | func didSelectCurveRow(request: SpringRequest) { 87 | animation.curve = curveList[request.rowIndex] 88 | presenter?.presentAnimation(response: response) 89 | } 90 | 91 | func forceSliderDidChanged(request: SpringRequest) { 92 | animation.force = Double(request.force) 93 | presenter?.presentAnimation(response: response) 94 | } 95 | 96 | func durationSliderDidChanged(request: SpringRequest) { 97 | animation.duration = Double(request.duration) 98 | presenter?.presentAnimation(response: response) 99 | } 100 | 101 | func delaySliderDidChanged(request: SpringRequest) { 102 | animation.delay = Double(request.delay) 103 | presenter?.presentAnimation(response: response) 104 | } 105 | 106 | func transformSpringViewButtonDidTapped() { 107 | isCircle.toggle() 108 | let halfSize = viewHeight / 2 109 | let cornerRadius = isCircle ? halfSize : 10 110 | 111 | let response = TransformResponse( 112 | keyPath: "cornerRadius", 113 | fromValue: isCircle ? 10 : halfSize, 114 | toValue: cornerRadius, 115 | duration: 0.2 116 | ) 117 | presenter?.presentTransformation(response: response) 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /SpringApp/Spring/SpringModels.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SpringModels.swift 3 | // SpringApp 4 | // 5 | // Created by Alexey Efimov on 16.04.2022. 6 | // Copyright (c) 2022 ___ORGANIZATIONNAME___. All rights reserved. 7 | // 8 | // This file was generated by the Clean Swift Xcode Templates so 9 | // you can apply clean architecture to your iOS and Mac projects, 10 | // see http://clean-swift.com 11 | // 12 | 13 | import QuartzCore 14 | 15 | typealias SpringRequest = Spring.Animate.Request 16 | typealias SpringResponse = Spring.Animate.Response 17 | typealias SpringViewModel = Spring.Animate.ViewModel 18 | 19 | typealias TransformRequest = Spring.TransformSpringView.Request 20 | typealias TransformResponse = Spring.TransformSpringView.Response 21 | typealias TransformViewModel = Spring.TransformSpringView.ViewModel 22 | 23 | enum Spring { 24 | 25 | // MARK: Animate Use case 26 | enum Animate { 27 | struct Request { 28 | var rowIndex = 0 29 | let autostart: Bool 30 | let autohide: Bool 31 | 32 | let title: String 33 | let curve: String 34 | 35 | var force: Double 36 | var delay: Double 37 | var duration: Double 38 | let damping: Double 39 | let velocity: Double 40 | let repeatCount: Float 41 | let x: Double 42 | let y: Double 43 | let scaleX: Double 44 | let scaleY: Double 45 | let scale: Double 46 | let rotate: Double 47 | } 48 | 49 | struct Response { 50 | let animation: Animation 51 | var animationList: [String] = [] 52 | var curveList: [String] = [] 53 | } 54 | 55 | struct ViewModel { 56 | let autostart: Bool 57 | let autohide: Bool 58 | 59 | let title: String 60 | let curve: String 61 | 62 | let force: Double 63 | let delay: Double 64 | let duration: Double 65 | let damping: Double 66 | let velocity: Double 67 | let repeatCount: Float 68 | let x: Double 69 | let y: Double 70 | let scaleX: Double 71 | let scaleY: Double 72 | let scale: Double 73 | let rotate: Double 74 | 75 | let forceText: String 76 | let durationText: String 77 | let delayText: String 78 | 79 | var animationList: [String] = [] 80 | var curveList: [String] = [] 81 | 82 | init(animation: Animation) { 83 | autostart = animation.autostart 84 | autohide = animation.autohide 85 | 86 | title = animation.title 87 | curve = animation.curve 88 | 89 | force = animation.force 90 | delay = animation.delay 91 | duration = animation.duration 92 | damping = animation.damping 93 | velocity = animation.velocity 94 | repeatCount = animation.repeatCount 95 | x = animation.x 96 | y = animation.y 97 | scaleX = animation.scaleX 98 | scaleY = animation.scaleY 99 | scale = animation.scale 100 | rotate = animation.rotate 101 | 102 | forceText = String(format: "Force: %.1f", animation.force) 103 | durationText = String(format: "Duration: %.1f", animation.duration) 104 | delayText = String(format: "Delay: %.1f", animation.delay) 105 | } 106 | } 107 | 108 | } 109 | 110 | // MARK: - Transform use case 111 | enum TransformSpringView { 112 | 113 | struct Request { 114 | var viewSize: Double = 0 115 | } 116 | 117 | struct Response { 118 | let keyPath: String 119 | let fromValue: Double 120 | let toValue: Double 121 | let duration: Double 122 | } 123 | 124 | struct ViewModel { 125 | let cornerRadius: Double 126 | let animation: CABasicAnimation 127 | let key: String 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /SpringApp/Code/CodeViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CodeViewController.swift 3 | // SpringApp 4 | // 5 | // Created by Alexey Efimov on 18.04.2022. 6 | // Copyright (c) 2022 ___ORGANIZATIONNAME___. All rights reserved. 7 | // 8 | // This file was generated by the Clean Swift Xcode Templates so 9 | // you can apply clean architecture to your iOS and Mac projects, 10 | // see http://clean-swift.com 11 | // 12 | 13 | import UIKit 14 | import SpringAnimation 15 | 16 | protocol CodeDisplayLogic: AnyObject { 17 | func displayCode(viewModel: CodeViewModel) 18 | } 19 | 20 | class CodeViewController: UIViewController { 21 | 22 | private var interactor: CodeBusinessLogic? 23 | var router: (NSObjectProtocol & CodeRoutingLogic & CodeDataPassing)? 24 | 25 | private lazy var modalView: SpringView = { 26 | let view = SpringView() 27 | view.isHidden = true 28 | return view 29 | }() 30 | 31 | private lazy var codeLabel: UILabel = { 32 | let label = UILabel() 33 | label.backgroundColor = UIColor(hex: "3D424E") 34 | label.text = "Code" 35 | label.textColor = UIColor(hex: "848CA0") 36 | label.font = UIFont(name: "Avenir Next Regular", size: 20) 37 | label.textAlignment = .center 38 | return label 39 | }() 40 | 41 | private lazy var codeTextView: UITextView = { 42 | let textView = UITextView() 43 | textView.backgroundColor = UIColor(hex: "3D424E") 44 | textView.isEditable = false 45 | textView.textColor = .white 46 | textView.font = UIFont(name: "Menlo Regular", size: 14) 47 | return textView 48 | }() 49 | 50 | // MARK: Object lifecycle 51 | override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { 52 | super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) 53 | setup() 54 | } 55 | 56 | required init?(coder aDecoder: NSCoder) { 57 | super.init(coder: aDecoder) 58 | setup() 59 | } 60 | 61 | override func loadView() { 62 | view = modalView 63 | } 64 | 65 | // MARK: View lifecycle 66 | override func viewDidLoad() { 67 | super.viewDidLoad() 68 | setupSubview(codeLabel, codeTextView) 69 | setupConstraints() 70 | interactor?.showCode() 71 | } 72 | 73 | override func viewDidAppear(_ animated: Bool) { 74 | super.viewDidAppear(animated) 75 | modalView.animation = "squeezeUp" 76 | modalView.isHidden = false 77 | modalView.animate() 78 | } 79 | 80 | // MARK: Routing 81 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) { 82 | if let scene = segue.identifier { 83 | let selector = NSSelectorFromString("routeTo\(scene)WithSegue:") 84 | if let router = router, router.responds(to: selector) { 85 | router.perform(selector, with: segue) 86 | } 87 | } 88 | } 89 | 90 | private func setupSubview(_ subviews: UIView...) { 91 | subviews.forEach { subview in 92 | view.addSubview(subview) 93 | } 94 | } 95 | 96 | private func setupConstraints() { 97 | codeLabel.translatesAutoresizingMaskIntoConstraints = false 98 | 99 | NSLayoutConstraint.activate([ 100 | codeLabel.topAnchor.constraint(equalTo: view.topAnchor, constant: 0), 101 | codeLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0), 102 | codeLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0), 103 | codeLabel.heightAnchor.constraint(equalToConstant: 80) 104 | ]) 105 | 106 | codeTextView.translatesAutoresizingMaskIntoConstraints = false 107 | 108 | NSLayoutConstraint.activate([ 109 | codeTextView.topAnchor.constraint(equalTo: codeLabel.bottomAnchor, constant: 0), 110 | codeTextView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0), 111 | codeTextView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0), 112 | codeTextView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 100) 113 | ]) 114 | } 115 | 116 | // MARK: Setup 117 | private func setup() { 118 | let viewController = self 119 | let interactor = CodeInteractor() 120 | let presenter = CodePresenter() 121 | let router = CodeRouter() 122 | viewController.interactor = interactor 123 | viewController.router = router 124 | interactor.presenter = presenter 125 | presenter.viewController = viewController 126 | router.viewController = viewController 127 | router.dataStore = interactor 128 | } 129 | } 130 | 131 | // MARK: - CodeDisplayLogic 132 | extension CodeViewController: CodeDisplayLogic { 133 | func displayCode(viewModel: CodeViewModel) { 134 | codeTextView.text = viewModel.codeText 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /SpringApp/Options/OptionsViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OptionsViewController.swift 3 | // SpringApp 4 | // 5 | // Created by Alexey Efimov on 17.04.2022. 6 | // Copyright (c) 2022 ___ORGANIZATIONNAME___. All rights reserved. 7 | // 8 | // This file was generated by the Clean Swift Xcode Templates so 9 | // you can apply clean architecture to your iOS and Mac projects, 10 | // see http://clean-swift.com 11 | // 12 | 13 | import UIKit 14 | import SpringAnimation 15 | 16 | protocol OptionsDisplayLogic: AnyObject { 17 | func displayOptions(viewModel: OptionsViewModel) 18 | } 19 | 20 | class OptionsViewController: UIViewController { 21 | 22 | @IBOutlet var modalView: SpringView! 23 | 24 | @IBOutlet var dampingLabel: UILabel! 25 | @IBOutlet var velocityLabel: UILabel! 26 | @IBOutlet var scaleLabel: UILabel! 27 | @IBOutlet var xLabel: UILabel! 28 | @IBOutlet var yLabel: UILabel! 29 | @IBOutlet var rotateLabel: UILabel! 30 | 31 | @IBOutlet var dampingSlider: UISlider! 32 | @IBOutlet var velocitySlider: UISlider! 33 | @IBOutlet var scaleSlider: UISlider! 34 | @IBOutlet var xSlider: UISlider! 35 | @IBOutlet var ySlider: UISlider! 36 | @IBOutlet var rotateSlider: UISlider! 37 | 38 | var router: (NSObjectProtocol & OptionsRoutingLogic & OptionsDataPassing)? 39 | var delegate: SpringDisplayLogic! 40 | 41 | private var interactor: OptionsBusinessLogic? 42 | private var request = OptionsRequest() 43 | 44 | // MARK: Object lifecycle 45 | override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { 46 | super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) 47 | setup() 48 | } 49 | 50 | required init?(coder aDecoder: NSCoder) { 51 | super.init(coder: aDecoder) 52 | setup() 53 | } 54 | 55 | override func viewDidLoad() { 56 | super.viewDidLoad() 57 | let tapGesture = UITapGestureRecognizer(target: self, action: #selector(tapAction)) 58 | view.addGestureRecognizer(tapGesture) 59 | interactor?.setOptions() 60 | } 61 | 62 | override func viewDidAppear(_ animated: Bool) { 63 | super.viewDidAppear(animated) 64 | modalView.animate() 65 | } 66 | 67 | @IBAction func optionSlider(_ sender: UISlider) { 68 | switch sender { 69 | case dampingSlider: 70 | request.damping = dampingSlider.value 71 | interactor?.dampingSliderDidChanged(request: request) 72 | case velocitySlider: 73 | request.velocity = velocitySlider.value 74 | interactor?.velocitySliderDidChanged(request: request) 75 | case scaleSlider: 76 | request.scale = scaleSlider.value 77 | interactor?.scaleSliderDidChanged(request: request) 78 | case xSlider: 79 | request.x = xSlider.value 80 | interactor?.xSliderDidChanged(request: request) 81 | case ySlider: 82 | request.y = ySlider.value 83 | interactor?.ySliderDidChanged(request: request) 84 | default: 85 | request.rotate = rotateSlider.value 86 | interactor?.rotateSliderDidChanged(request: request) 87 | } 88 | 89 | } 90 | 91 | @IBAction func resetButtonPressed() { 92 | interactor?.resetButtonPressed() 93 | } 94 | 95 | @objc private func tapAction() { 96 | modalView.animation = "slideUp" 97 | modalView.animateToNext { 98 | self.dismiss(animated: false) 99 | } 100 | } 101 | 102 | // MARK: Setup 103 | private func setup() { 104 | let viewController = self 105 | let interactor = OptionsInteractor() 106 | let presenter = OptionsPresenter() 107 | let router = OptionsRouter() 108 | viewController.interactor = interactor 109 | viewController.router = router 110 | interactor.presenter = presenter 111 | presenter.viewController = viewController 112 | router.viewController = viewController 113 | router.dataStore = interactor 114 | } 115 | } 116 | 117 | // MARK: - OptionsDisplayLogic 118 | extension OptionsViewController: OptionsDisplayLogic { 119 | func displayOptions(viewModel: OptionsViewModel) { 120 | dampingSlider.setValue(Float(viewModel.damping), animated: true) 121 | velocitySlider.setValue(Float(viewModel.velocity), animated: true) 122 | scaleSlider.setValue(Float(viewModel.scale), animated: true) 123 | xSlider.setValue(Float(viewModel.x), animated: true) 124 | ySlider.setValue(Float(viewModel.y), animated: true) 125 | rotateSlider.setValue(Float(viewModel.rotate), animated: true) 126 | 127 | dampingLabel.text = viewModel.dampingText 128 | velocityLabel.text = viewModel.velocityText 129 | scaleLabel.text = viewModel.scaleText 130 | xLabel.text = viewModel.xText 131 | yLabel.text = viewModel.yText 132 | rotateLabel.text = viewModel.rotateText 133 | 134 | let animation = Animation( 135 | autostart: viewModel.autostart, 136 | autohide: viewModel.autohide, 137 | title: viewModel.title, 138 | curve: viewModel.curve, 139 | force: viewModel.force, 140 | delay: viewModel.delay, 141 | duration: viewModel.duration, 142 | damping: viewModel.damping, 143 | velocity: viewModel.velocity, 144 | repeatCount: viewModel.repeatCount, 145 | x: viewModel.x, 146 | y: viewModel.y, 147 | scaleX: viewModel.scaleX, 148 | scaleY: viewModel.scaleY, 149 | scale: viewModel.scale, 150 | rotate: viewModel.rotate 151 | ) 152 | 153 | let springViewModel = SpringViewModel(animation: animation) 154 | delegate.displayAnimation(viewModel: springViewModel) 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /SpringApp/Spring/SpringViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SpringViewController.swift 3 | // SpringApp 4 | // 5 | // Created by Alexey Efimov on 16.04.2022. 6 | // 7 | 8 | import UIKit 9 | import SpringAnimation 10 | 11 | protocol SpringDisplayLogic: AnyObject { 12 | func displayInitialValues(viewModel: SpringViewModel) 13 | func displayAnimation(viewModel: SpringViewModel) 14 | func displayTransformation(viewModel: TransformViewModel) 15 | } 16 | 17 | class SpringViewController: UIViewController { 18 | 19 | @IBOutlet var delayLabel: UILabel! 20 | @IBOutlet var durationLabel: UILabel! 21 | @IBOutlet var forceLabel: UILabel! 22 | @IBOutlet var delaySlider: UISlider! 23 | @IBOutlet var durationSlider: UISlider! 24 | @IBOutlet var forceSlider: UISlider! 25 | 26 | @IBOutlet var pickerView: UIPickerView! 27 | 28 | @IBOutlet var springView: SpringView! 29 | 30 | private var interactor: SpringBusinessLogic? 31 | private var router: (NSObjectProtocol & SpringRoutingLogic & SpringDataPassing)? 32 | 33 | private var request: SpringRequest! 34 | 35 | private var animationList: [String] = [] 36 | private var curveList: [String] = [] 37 | 38 | // MARK: Object lifecycle 39 | override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { 40 | super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) 41 | setup() 42 | } 43 | 44 | required init?(coder aDecoder: NSCoder) { 45 | super.init(coder: aDecoder) 46 | setup() 47 | } 48 | 49 | // MARK: - View lifecycle 50 | override func viewDidLoad() { 51 | super.viewDidLoad() 52 | let tapGesture = UITapGestureRecognizer(target: self, action: #selector(tapAction)) 53 | view.addGestureRecognizer(tapGesture) 54 | request = SpringRequest( 55 | autostart: springView.autostart, 56 | autohide: springView.autohide, 57 | title: springView.animation, 58 | curve: springView.curve, 59 | force: springView.force, 60 | delay: springView.delay, 61 | duration: springView.duration, 62 | damping: springView.damping, 63 | velocity: springView.velocity, 64 | repeatCount: springView.repeatCount, 65 | x: springView.x, 66 | y: springView.y, 67 | scaleX: springView.scaleX, 68 | scaleY: springView.scaleY, 69 | scale: 1, 70 | rotate: springView.rotate 71 | ) 72 | interactor?.setInitialValues(request: request) 73 | } 74 | 75 | override func viewWillLayoutSubviews() { 76 | super.viewWillLayoutSubviews() 77 | let viewSize = springView.frame.height 78 | let request = TransformRequest(viewSize: viewSize) 79 | interactor?.setViewSize(request: request) 80 | } 81 | 82 | // MARK: Routing 83 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) { 84 | if let scene = segue.identifier { 85 | let selector = NSSelectorFromString("routeTo\(scene)WithSegue:") 86 | if let router = router, router.responds(to: selector) { 87 | router.perform(selector, with: segue) 88 | } 89 | } 90 | } 91 | 92 | // MARK: - IBActions 93 | @IBAction func codeButtonPressed() { 94 | router?.routeToCode() 95 | } 96 | 97 | @IBAction func sliderAction(_ sender: UISlider) { 98 | switch sender { 99 | case forceSlider: 100 | request.force = Double(forceSlider.value) 101 | interactor?.forceSliderDidChanged(request: request) 102 | case durationSlider: 103 | request.duration = Double(durationSlider.value) 104 | interactor?.durationSliderDidChanged(request: request) 105 | default: 106 | request.delay = Double(delaySlider.value) 107 | interactor?.delaySliderDidChanged(request: request) 108 | 109 | } 110 | } 111 | 112 | @IBAction func transformSpringView() { 113 | interactor?.transformSpringViewButtonDidTapped() 114 | } 115 | 116 | @objc private func tapAction() { 117 | interactor?.didTapView() 118 | } 119 | 120 | // MARK: Setup 121 | private func setup() { 122 | let viewController = self 123 | let interactor = SpringInteractor() 124 | let presenter = SpringPresenter() 125 | let router = SpringRouter() 126 | viewController.interactor = interactor 127 | viewController.router = router 128 | interactor.presenter = presenter 129 | presenter.viewController = viewController 130 | router.springVC = viewController 131 | router.dataStore = interactor 132 | } 133 | } 134 | 135 | // MARK: - SpringDisplayLogic 136 | extension SpringViewController: SpringDisplayLogic { 137 | func displayInitialValues(viewModel: SpringViewModel) { 138 | forceLabel.text = viewModel.forceText 139 | durationLabel.text = viewModel.durationText 140 | delayLabel.text = viewModel.delayText 141 | 142 | animationList = viewModel.animationList 143 | curveList = viewModel.curveList 144 | } 145 | 146 | func displayAnimation(viewModel: SpringViewModel) { 147 | forceLabel.text = viewModel.forceText 148 | durationLabel.text = viewModel.durationText 149 | delayLabel.text = viewModel.delayText 150 | 151 | let animationIndex = animationList.firstIndex(of: viewModel.title) ?? 0 152 | let curveIndex = curveList.firstIndex(of: viewModel.curve) ?? 0 153 | pickerView.selectRow(animationIndex, inComponent: 0, animated: false) 154 | pickerView.selectRow(curveIndex, inComponent: 1, animated: false) 155 | 156 | springView.animation = viewModel.title 157 | springView.curve = viewModel.curve 158 | springView.force = viewModel.force 159 | springView.duration = viewModel.duration 160 | springView.delay = viewModel.delay 161 | springView.damping = viewModel.damping 162 | springView.velocity = viewModel.velocity 163 | springView.scaleX = viewModel.scale 164 | springView.scaleY = viewModel.scale 165 | springView.x = viewModel.x 166 | springView.y = viewModel.y 167 | springView.rotate = viewModel.rotate 168 | springView.animate() 169 | } 170 | 171 | func displayTransformation(viewModel: TransformViewModel) { 172 | springView.layer.cornerRadius = viewModel.cornerRadius 173 | springView.layer.add(viewModel.animation, forKey: viewModel.key) 174 | } 175 | } 176 | 177 | // MARK: - UIPickerViewDataSource, UIPickerViewDelegate 178 | extension SpringViewController: UIPickerViewDataSource, UIPickerViewDelegate { 179 | func numberOfComponents(in pickerView: UIPickerView) -> Int { 180 | 2 181 | } 182 | 183 | func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { 184 | component == 0 ? animationList.count : curveList.count 185 | } 186 | 187 | func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { 188 | component == 0 ? animationList[row] : curveList[row] 189 | } 190 | 191 | func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { 192 | switch component { 193 | case 0: 194 | request.rowIndex = row 195 | interactor?.didSelectAnimationRow(request: request) 196 | default: 197 | request.rowIndex = row 198 | interactor?.didSelectCurveRow(request: request) 199 | } 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /SpringAnimation/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 customAwakeFromNib() { 19 | if view.autohide { 20 | view.alpha = 0 21 | } 22 | } 23 | 24 | public func customLayoutSubviews() { 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 | -------------------------------------------------------------------------------- /SpringApp.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 55; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | A85E83E2280DA79E00FB6B82 /* CodePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A85E83DC280DA79E00FB6B82 /* CodePresenter.swift */; }; 11 | A85E83E4280DA79E00FB6B82 /* CodeRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A85E83DE280DA79E00FB6B82 /* CodeRouter.swift */; }; 12 | A85E83E5280DA79E00FB6B82 /* CodeModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = A85E83DF280DA79E00FB6B82 /* CodeModels.swift */; }; 13 | A85E83E6280DA79E00FB6B82 /* CodeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A85E83E0280DA79E00FB6B82 /* CodeViewController.swift */; }; 14 | A85E83E7280DA79E00FB6B82 /* CodeInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A85E83E1280DA79E00FB6B82 /* CodeInteractor.swift */; }; 15 | A86038C3280ACEB40058D49E /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A86038C2280ACEB40058D49E /* AppDelegate.swift */; }; 16 | A86038C5280ACEB40058D49E /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A86038C4280ACEB40058D49E /* SceneDelegate.swift */; }; 17 | A86038C7280ACEB40058D49E /* SpringViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A86038C6280ACEB40058D49E /* SpringViewController.swift */; }; 18 | A86038CA280ACEB40058D49E /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A86038C8280ACEB40058D49E /* Main.storyboard */; }; 19 | A86038CC280ACEB50058D49E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A86038CB280ACEB50058D49E /* Assets.xcassets */; }; 20 | A86038CF280ACEB50058D49E /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A86038CD280ACEB50058D49E /* LaunchScreen.storyboard */; }; 21 | A86038E1280AD7BB0058D49E /* Animation.swift in Sources */ = {isa = PBXBuildFile; fileRef = A86038E0280AD7BB0058D49E /* Animation.swift */; }; 22 | A86038E9280ADEAA0058D49E /* SpringAnimation in Frameworks */ = {isa = PBXBuildFile; productRef = A86038E8280ADEAA0058D49E /* SpringAnimation */; }; 23 | A86038EF280AE2410058D49E /* SpringPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A86038EA280AE2410058D49E /* SpringPresenter.swift */; }; 24 | A86038F1280AE2410058D49E /* SpringRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A86038EC280AE2410058D49E /* SpringRouter.swift */; }; 25 | A86038F2280AE2410058D49E /* SpringModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = A86038ED280AE2410058D49E /* SpringModels.swift */; }; 26 | A86038F3280AE2410058D49E /* SpringInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A86038EE280AE2410058D49E /* SpringInteractor.swift */; }; 27 | A8603909280C36070058D49E /* OptionsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8603903280C36070058D49E /* OptionsPresenter.swift */; }; 28 | A860390B280C36070058D49E /* OptionsRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8603905280C36070058D49E /* OptionsRouter.swift */; }; 29 | A860390C280C36070058D49E /* OptionsModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8603906280C36070058D49E /* OptionsModels.swift */; }; 30 | A860390D280C36070058D49E /* OptionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8603907280C36070058D49E /* OptionsViewController.swift */; }; 31 | A860390E280C36070058D49E /* OptionsInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8603908280C36070058D49E /* OptionsInteractor.swift */; }; 32 | /* End PBXBuildFile section */ 33 | 34 | /* Begin PBXFileReference section */ 35 | A85E83DC280DA79E00FB6B82 /* CodePresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodePresenter.swift; sourceTree = ""; }; 36 | A85E83DE280DA79E00FB6B82 /* CodeRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodeRouter.swift; sourceTree = ""; }; 37 | A85E83DF280DA79E00FB6B82 /* CodeModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodeModels.swift; sourceTree = ""; }; 38 | A85E83E0280DA79E00FB6B82 /* CodeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodeViewController.swift; sourceTree = ""; }; 39 | A85E83E1280DA79E00FB6B82 /* CodeInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodeInteractor.swift; sourceTree = ""; }; 40 | A86038BF280ACEB40058D49E /* SpringApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SpringApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; 41 | A86038C2280ACEB40058D49E /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 42 | A86038C4280ACEB40058D49E /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 43 | A86038C6280ACEB40058D49E /* SpringViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpringViewController.swift; sourceTree = ""; }; 44 | A86038C9280ACEB40058D49E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 45 | A86038CB280ACEB50058D49E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 46 | A86038CE280ACEB50058D49E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 47 | A86038D0280ACEB50058D49E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 48 | A86038E0280AD7BB0058D49E /* Animation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Animation.swift; sourceTree = ""; }; 49 | A86038E7280ADEA30058D49E /* SpringAnimation */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = SpringAnimation; sourceTree = ""; }; 50 | A86038EA280AE2410058D49E /* SpringPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpringPresenter.swift; sourceTree = ""; }; 51 | A86038EC280AE2410058D49E /* SpringRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpringRouter.swift; sourceTree = ""; }; 52 | A86038ED280AE2410058D49E /* SpringModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpringModels.swift; sourceTree = ""; }; 53 | A86038EE280AE2410058D49E /* SpringInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpringInteractor.swift; sourceTree = ""; }; 54 | A8603903280C36070058D49E /* OptionsPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionsPresenter.swift; sourceTree = ""; }; 55 | A8603905280C36070058D49E /* OptionsRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionsRouter.swift; sourceTree = ""; }; 56 | A8603906280C36070058D49E /* OptionsModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionsModels.swift; sourceTree = ""; }; 57 | A8603907280C36070058D49E /* OptionsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionsViewController.swift; sourceTree = ""; }; 58 | A8603908280C36070058D49E /* OptionsInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionsInteractor.swift; sourceTree = ""; }; 59 | /* End PBXFileReference section */ 60 | 61 | /* Begin PBXFrameworksBuildPhase section */ 62 | A86038BC280ACEB40058D49E /* Frameworks */ = { 63 | isa = PBXFrameworksBuildPhase; 64 | buildActionMask = 2147483647; 65 | files = ( 66 | A86038E9280ADEAA0058D49E /* SpringAnimation in Frameworks */, 67 | ); 68 | runOnlyForDeploymentPostprocessing = 0; 69 | }; 70 | /* End PBXFrameworksBuildPhase section */ 71 | 72 | /* Begin PBXGroup section */ 73 | A85E83DB280DA77600FB6B82 /* Code */ = { 74 | isa = PBXGroup; 75 | children = ( 76 | A85E83E0280DA79E00FB6B82 /* CodeViewController.swift */, 77 | A85E83E1280DA79E00FB6B82 /* CodeInteractor.swift */, 78 | A85E83DC280DA79E00FB6B82 /* CodePresenter.swift */, 79 | A85E83DE280DA79E00FB6B82 /* CodeRouter.swift */, 80 | A85E83DF280DA79E00FB6B82 /* CodeModels.swift */, 81 | ); 82 | path = Code; 83 | sourceTree = ""; 84 | }; 85 | A86038B6280ACEB40058D49E = { 86 | isa = PBXGroup; 87 | children = ( 88 | A86038C1280ACEB40058D49E /* SpringApp */, 89 | A86038C0280ACEB40058D49E /* Products */, 90 | A86038D8280AD1890058D49E /* Frameworks */, 91 | ); 92 | sourceTree = ""; 93 | }; 94 | A86038C0280ACEB40058D49E /* Products */ = { 95 | isa = PBXGroup; 96 | children = ( 97 | A86038BF280ACEB40058D49E /* SpringApp.app */, 98 | ); 99 | name = Products; 100 | sourceTree = ""; 101 | }; 102 | A86038C1280ACEB40058D49E /* SpringApp */ = { 103 | isa = PBXGroup; 104 | children = ( 105 | A86038DC280AD7390058D49E /* Spring */, 106 | A8603902280C35D20058D49E /* Options */, 107 | A85E83DB280DA77600FB6B82 /* Code */, 108 | A86038DF280AD78B0058D49E /* Models */, 109 | A86038DE280AD75C0058D49E /* StoryBoards */, 110 | A86038DD280AD7490058D49E /* Resources */, 111 | A86038DB280AD71C0058D49E /* App */, 112 | ); 113 | path = SpringApp; 114 | sourceTree = ""; 115 | }; 116 | A86038D8280AD1890058D49E /* Frameworks */ = { 117 | isa = PBXGroup; 118 | children = ( 119 | A86038E7280ADEA30058D49E /* SpringAnimation */, 120 | ); 121 | name = Frameworks; 122 | sourceTree = ""; 123 | }; 124 | A86038DB280AD71C0058D49E /* App */ = { 125 | isa = PBXGroup; 126 | children = ( 127 | A86038C2280ACEB40058D49E /* AppDelegate.swift */, 128 | A86038C4280ACEB40058D49E /* SceneDelegate.swift */, 129 | A86038D0280ACEB50058D49E /* Info.plist */, 130 | ); 131 | path = App; 132 | sourceTree = ""; 133 | }; 134 | A86038DC280AD7390058D49E /* Spring */ = { 135 | isa = PBXGroup; 136 | children = ( 137 | A86038C6280ACEB40058D49E /* SpringViewController.swift */, 138 | A86038EE280AE2410058D49E /* SpringInteractor.swift */, 139 | A86038EA280AE2410058D49E /* SpringPresenter.swift */, 140 | A86038EC280AE2410058D49E /* SpringRouter.swift */, 141 | A86038ED280AE2410058D49E /* SpringModels.swift */, 142 | ); 143 | path = Spring; 144 | sourceTree = ""; 145 | }; 146 | A86038DD280AD7490058D49E /* Resources */ = { 147 | isa = PBXGroup; 148 | children = ( 149 | A86038CB280ACEB50058D49E /* Assets.xcassets */, 150 | ); 151 | path = Resources; 152 | sourceTree = ""; 153 | }; 154 | A86038DE280AD75C0058D49E /* StoryBoards */ = { 155 | isa = PBXGroup; 156 | children = ( 157 | A86038C8280ACEB40058D49E /* Main.storyboard */, 158 | A86038CD280ACEB50058D49E /* LaunchScreen.storyboard */, 159 | ); 160 | path = StoryBoards; 161 | sourceTree = ""; 162 | }; 163 | A86038DF280AD78B0058D49E /* Models */ = { 164 | isa = PBXGroup; 165 | children = ( 166 | A86038E0280AD7BB0058D49E /* Animation.swift */, 167 | ); 168 | path = Models; 169 | sourceTree = ""; 170 | }; 171 | A8603902280C35D20058D49E /* Options */ = { 172 | isa = PBXGroup; 173 | children = ( 174 | A8603907280C36070058D49E /* OptionsViewController.swift */, 175 | A8603908280C36070058D49E /* OptionsInteractor.swift */, 176 | A8603903280C36070058D49E /* OptionsPresenter.swift */, 177 | A8603905280C36070058D49E /* OptionsRouter.swift */, 178 | A8603906280C36070058D49E /* OptionsModels.swift */, 179 | ); 180 | path = Options; 181 | sourceTree = ""; 182 | }; 183 | /* End PBXGroup section */ 184 | 185 | /* Begin PBXNativeTarget section */ 186 | A86038BE280ACEB40058D49E /* SpringApp */ = { 187 | isa = PBXNativeTarget; 188 | buildConfigurationList = A86038D3280ACEB50058D49E /* Build configuration list for PBXNativeTarget "SpringApp" */; 189 | buildPhases = ( 190 | A86038BB280ACEB40058D49E /* Sources */, 191 | A86038BC280ACEB40058D49E /* Frameworks */, 192 | A86038BD280ACEB40058D49E /* Resources */, 193 | ); 194 | buildRules = ( 195 | ); 196 | dependencies = ( 197 | ); 198 | name = SpringApp; 199 | packageProductDependencies = ( 200 | A86038E8280ADEAA0058D49E /* SpringAnimation */, 201 | ); 202 | productName = SpringApp; 203 | productReference = A86038BF280ACEB40058D49E /* SpringApp.app */; 204 | productType = "com.apple.product-type.application"; 205 | }; 206 | /* End PBXNativeTarget section */ 207 | 208 | /* Begin PBXProject section */ 209 | A86038B7280ACEB40058D49E /* Project object */ = { 210 | isa = PBXProject; 211 | attributes = { 212 | BuildIndependentTargetsInParallel = 1; 213 | LastSwiftUpdateCheck = 1330; 214 | LastUpgradeCheck = 1330; 215 | TargetAttributes = { 216 | A86038BE280ACEB40058D49E = { 217 | CreatedOnToolsVersion = 13.3.1; 218 | }; 219 | }; 220 | }; 221 | buildConfigurationList = A86038BA280ACEB40058D49E /* Build configuration list for PBXProject "SpringApp" */; 222 | compatibilityVersion = "Xcode 13.0"; 223 | developmentRegion = en; 224 | hasScannedForEncodings = 0; 225 | knownRegions = ( 226 | en, 227 | Base, 228 | ); 229 | mainGroup = A86038B6280ACEB40058D49E; 230 | productRefGroup = A86038C0280ACEB40058D49E /* Products */; 231 | projectDirPath = ""; 232 | projectRoot = ""; 233 | targets = ( 234 | A86038BE280ACEB40058D49E /* SpringApp */, 235 | ); 236 | }; 237 | /* End PBXProject section */ 238 | 239 | /* Begin PBXResourcesBuildPhase section */ 240 | A86038BD280ACEB40058D49E /* Resources */ = { 241 | isa = PBXResourcesBuildPhase; 242 | buildActionMask = 2147483647; 243 | files = ( 244 | A86038CF280ACEB50058D49E /* LaunchScreen.storyboard in Resources */, 245 | A86038CC280ACEB50058D49E /* Assets.xcassets in Resources */, 246 | A86038CA280ACEB40058D49E /* Main.storyboard in Resources */, 247 | ); 248 | runOnlyForDeploymentPostprocessing = 0; 249 | }; 250 | /* End PBXResourcesBuildPhase section */ 251 | 252 | /* Begin PBXSourcesBuildPhase section */ 253 | A86038BB280ACEB40058D49E /* Sources */ = { 254 | isa = PBXSourcesBuildPhase; 255 | buildActionMask = 2147483647; 256 | files = ( 257 | A85E83E5280DA79E00FB6B82 /* CodeModels.swift in Sources */, 258 | A86038C7280ACEB40058D49E /* SpringViewController.swift in Sources */, 259 | A85E83E2280DA79E00FB6B82 /* CodePresenter.swift in Sources */, 260 | A85E83E4280DA79E00FB6B82 /* CodeRouter.swift in Sources */, 261 | A86038F1280AE2410058D49E /* SpringRouter.swift in Sources */, 262 | A860390D280C36070058D49E /* OptionsViewController.swift in Sources */, 263 | A860390B280C36070058D49E /* OptionsRouter.swift in Sources */, 264 | A85E83E7280DA79E00FB6B82 /* CodeInteractor.swift in Sources */, 265 | A860390C280C36070058D49E /* OptionsModels.swift in Sources */, 266 | A860390E280C36070058D49E /* OptionsInteractor.swift in Sources */, 267 | A86038C3280ACEB40058D49E /* AppDelegate.swift in Sources */, 268 | A86038EF280AE2410058D49E /* SpringPresenter.swift in Sources */, 269 | A85E83E6280DA79E00FB6B82 /* CodeViewController.swift in Sources */, 270 | A86038F3280AE2410058D49E /* SpringInteractor.swift in Sources */, 271 | A86038E1280AD7BB0058D49E /* Animation.swift in Sources */, 272 | A86038F2280AE2410058D49E /* SpringModels.swift in Sources */, 273 | A8603909280C36070058D49E /* OptionsPresenter.swift in Sources */, 274 | A86038C5280ACEB40058D49E /* SceneDelegate.swift in Sources */, 275 | ); 276 | runOnlyForDeploymentPostprocessing = 0; 277 | }; 278 | /* End PBXSourcesBuildPhase section */ 279 | 280 | /* Begin PBXVariantGroup section */ 281 | A86038C8280ACEB40058D49E /* Main.storyboard */ = { 282 | isa = PBXVariantGroup; 283 | children = ( 284 | A86038C9280ACEB40058D49E /* Base */, 285 | ); 286 | name = Main.storyboard; 287 | sourceTree = ""; 288 | }; 289 | A86038CD280ACEB50058D49E /* LaunchScreen.storyboard */ = { 290 | isa = PBXVariantGroup; 291 | children = ( 292 | A86038CE280ACEB50058D49E /* Base */, 293 | ); 294 | name = LaunchScreen.storyboard; 295 | sourceTree = ""; 296 | }; 297 | /* End PBXVariantGroup section */ 298 | 299 | /* Begin XCBuildConfiguration section */ 300 | A86038D1280ACEB50058D49E /* Debug */ = { 301 | isa = XCBuildConfiguration; 302 | buildSettings = { 303 | ALWAYS_SEARCH_USER_PATHS = NO; 304 | CLANG_ANALYZER_NONNULL = YES; 305 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 306 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 307 | CLANG_ENABLE_MODULES = YES; 308 | CLANG_ENABLE_OBJC_ARC = YES; 309 | CLANG_ENABLE_OBJC_WEAK = YES; 310 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 311 | CLANG_WARN_BOOL_CONVERSION = YES; 312 | CLANG_WARN_COMMA = YES; 313 | CLANG_WARN_CONSTANT_CONVERSION = YES; 314 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 315 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 316 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 317 | CLANG_WARN_EMPTY_BODY = YES; 318 | CLANG_WARN_ENUM_CONVERSION = YES; 319 | CLANG_WARN_INFINITE_RECURSION = YES; 320 | CLANG_WARN_INT_CONVERSION = YES; 321 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 322 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 323 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 324 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 325 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 326 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 327 | CLANG_WARN_STRICT_PROTOTYPES = YES; 328 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 329 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 330 | CLANG_WARN_UNREACHABLE_CODE = YES; 331 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 332 | COPY_PHASE_STRIP = NO; 333 | DEBUG_INFORMATION_FORMAT = dwarf; 334 | ENABLE_STRICT_OBJC_MSGSEND = YES; 335 | ENABLE_TESTABILITY = YES; 336 | GCC_C_LANGUAGE_STANDARD = gnu11; 337 | GCC_DYNAMIC_NO_PIC = NO; 338 | GCC_NO_COMMON_BLOCKS = YES; 339 | GCC_OPTIMIZATION_LEVEL = 0; 340 | GCC_PREPROCESSOR_DEFINITIONS = ( 341 | "DEBUG=1", 342 | "$(inherited)", 343 | ); 344 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 345 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 346 | GCC_WARN_UNDECLARED_SELECTOR = YES; 347 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 348 | GCC_WARN_UNUSED_FUNCTION = YES; 349 | GCC_WARN_UNUSED_VARIABLE = YES; 350 | IPHONEOS_DEPLOYMENT_TARGET = 15.4; 351 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 352 | MTL_FAST_MATH = YES; 353 | ONLY_ACTIVE_ARCH = YES; 354 | SDKROOT = iphoneos; 355 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 356 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 357 | }; 358 | name = Debug; 359 | }; 360 | A86038D2280ACEB50058D49E /* Release */ = { 361 | isa = XCBuildConfiguration; 362 | buildSettings = { 363 | ALWAYS_SEARCH_USER_PATHS = NO; 364 | CLANG_ANALYZER_NONNULL = YES; 365 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 366 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 367 | CLANG_ENABLE_MODULES = YES; 368 | CLANG_ENABLE_OBJC_ARC = YES; 369 | CLANG_ENABLE_OBJC_WEAK = YES; 370 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 371 | CLANG_WARN_BOOL_CONVERSION = YES; 372 | CLANG_WARN_COMMA = YES; 373 | CLANG_WARN_CONSTANT_CONVERSION = YES; 374 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 375 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 376 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 377 | CLANG_WARN_EMPTY_BODY = YES; 378 | CLANG_WARN_ENUM_CONVERSION = YES; 379 | CLANG_WARN_INFINITE_RECURSION = YES; 380 | CLANG_WARN_INT_CONVERSION = YES; 381 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 382 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 383 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 384 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 385 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 386 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 387 | CLANG_WARN_STRICT_PROTOTYPES = YES; 388 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 389 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 390 | CLANG_WARN_UNREACHABLE_CODE = YES; 391 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 392 | COPY_PHASE_STRIP = NO; 393 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 394 | ENABLE_NS_ASSERTIONS = NO; 395 | ENABLE_STRICT_OBJC_MSGSEND = YES; 396 | GCC_C_LANGUAGE_STANDARD = gnu11; 397 | GCC_NO_COMMON_BLOCKS = YES; 398 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 399 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 400 | GCC_WARN_UNDECLARED_SELECTOR = YES; 401 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 402 | GCC_WARN_UNUSED_FUNCTION = YES; 403 | GCC_WARN_UNUSED_VARIABLE = YES; 404 | IPHONEOS_DEPLOYMENT_TARGET = 15.4; 405 | MTL_ENABLE_DEBUG_INFO = NO; 406 | MTL_FAST_MATH = YES; 407 | SDKROOT = iphoneos; 408 | SWIFT_COMPILATION_MODE = wholemodule; 409 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 410 | VALIDATE_PRODUCT = YES; 411 | }; 412 | name = Release; 413 | }; 414 | A86038D4280ACEB50058D49E /* Debug */ = { 415 | isa = XCBuildConfiguration; 416 | buildSettings = { 417 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 418 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 419 | CODE_SIGN_STYLE = Automatic; 420 | CURRENT_PROJECT_VERSION = 1; 421 | DEVELOPMENT_TEAM = GBTTU32HS2; 422 | GENERATE_INFOPLIST_FILE = YES; 423 | INFOPLIST_FILE = SpringApp/App/Info.plist; 424 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 425 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; 426 | INFOPLIST_KEY_UIMainStoryboardFile = Main; 427 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 428 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 429 | LD_RUNPATH_SEARCH_PATHS = ( 430 | "$(inherited)", 431 | "@executable_path/Frameworks", 432 | ); 433 | MARKETING_VERSION = 1.0; 434 | PRODUCT_BUNDLE_IDENTIFIER = LexDeBash.SpringApp; 435 | PRODUCT_NAME = "$(TARGET_NAME)"; 436 | SWIFT_EMIT_LOC_STRINGS = YES; 437 | SWIFT_VERSION = 5.0; 438 | TARGETED_DEVICE_FAMILY = "1,2"; 439 | }; 440 | name = Debug; 441 | }; 442 | A86038D5280ACEB50058D49E /* Release */ = { 443 | isa = XCBuildConfiguration; 444 | buildSettings = { 445 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 446 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 447 | CODE_SIGN_STYLE = Automatic; 448 | CURRENT_PROJECT_VERSION = 1; 449 | DEVELOPMENT_TEAM = GBTTU32HS2; 450 | GENERATE_INFOPLIST_FILE = YES; 451 | INFOPLIST_FILE = SpringApp/App/Info.plist; 452 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 453 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; 454 | INFOPLIST_KEY_UIMainStoryboardFile = Main; 455 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 456 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 457 | LD_RUNPATH_SEARCH_PATHS = ( 458 | "$(inherited)", 459 | "@executable_path/Frameworks", 460 | ); 461 | MARKETING_VERSION = 1.0; 462 | PRODUCT_BUNDLE_IDENTIFIER = LexDeBash.SpringApp; 463 | PRODUCT_NAME = "$(TARGET_NAME)"; 464 | SWIFT_EMIT_LOC_STRINGS = YES; 465 | SWIFT_VERSION = 5.0; 466 | TARGETED_DEVICE_FAMILY = "1,2"; 467 | }; 468 | name = Release; 469 | }; 470 | /* End XCBuildConfiguration section */ 471 | 472 | /* Begin XCConfigurationList section */ 473 | A86038BA280ACEB40058D49E /* Build configuration list for PBXProject "SpringApp" */ = { 474 | isa = XCConfigurationList; 475 | buildConfigurations = ( 476 | A86038D1280ACEB50058D49E /* Debug */, 477 | A86038D2280ACEB50058D49E /* Release */, 478 | ); 479 | defaultConfigurationIsVisible = 0; 480 | defaultConfigurationName = Release; 481 | }; 482 | A86038D3280ACEB50058D49E /* Build configuration list for PBXNativeTarget "SpringApp" */ = { 483 | isa = XCConfigurationList; 484 | buildConfigurations = ( 485 | A86038D4280ACEB50058D49E /* Debug */, 486 | A86038D5280ACEB50058D49E /* Release */, 487 | ); 488 | defaultConfigurationIsVisible = 0; 489 | defaultConfigurationName = Release; 490 | }; 491 | /* End XCConfigurationList section */ 492 | 493 | /* Begin XCSwiftPackageProductDependency section */ 494 | A86038E8280ADEAA0058D49E /* SpringAnimation */ = { 495 | isa = XCSwiftPackageProductDependency; 496 | productName = SpringAnimation; 497 | }; 498 | /* End XCSwiftPackageProductDependency section */ 499 | }; 500 | rootObject = A86038B7280ACEB40058D49E /* Project object */; 501 | } 502 | -------------------------------------------------------------------------------- /SpringApp/StoryBoards/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | AvenirNext-DemiBold 13 | AvenirNext-Regular 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 44 | 50 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 137 | 145 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 317 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | --------------------------------------------------------------------------------