├── 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 |
--------------------------------------------------------------------------------