├── TBIconTransitionKitExample
├── TBIconTransitionKitExample
│ ├── Assets.xcassets
│ │ ├── Contents.json
│ │ ├── AccentColor.colorset
│ │ │ └── Contents.json
│ │ └── AppIcon.appiconset
│ │ │ └── Contents.json
│ ├── Preview Content
│ │ └── Preview Assets.xcassets
│ │ │ └── Contents.json
│ ├── TBIconTransitionKitExampleApp.swift
│ ├── TBIconTransitionKitExample.entitlements
│ └── ContentView.swift
└── TBIconTransitionKitExample.xcodeproj
│ ├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
│ └── project.pbxproj
├── .swiftpm
└── xcode
│ └── package.xcworkspace
│ └── contents.xcworkspacedata
├── Package.swift
├── .gitignore
├── LICENSE
├── README.md
└── Sources
└── TBIconTransitionKit
└── AnimationButton.swift
/TBIconTransitionKitExample/TBIconTransitionKitExample/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/TBIconTransitionKitExample/TBIconTransitionKitExample/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/TBIconTransitionKitExample/TBIconTransitionKitExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/TBIconTransitionKitExample/TBIconTransitionKitExample/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 |
--------------------------------------------------------------------------------
/TBIconTransitionKitExample/TBIconTransitionKitExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/TBIconTransitionKitExample/TBIconTransitionKitExample/TBIconTransitionKitExampleApp.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TBIconTransitionKitExampleApp.swift
3 | // TBIconTransitionKitExample
4 | //
5 | // Created by Aleksei Belezeko on 18.04.2023.
6 | //
7 |
8 | import SwiftUI
9 |
10 | @main
11 | struct TBIconTransitionKitExampleApp: App {
12 | var body: some Scene {
13 | WindowGroup {
14 | ContentView()
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/TBIconTransitionKitExample/TBIconTransitionKitExample/TBIconTransitionKitExample.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 | com.apple.security.files.user-selected.read-only
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.5
2 |
3 | import PackageDescription
4 |
5 | let package = Package(
6 | name: "TBIconTransitionKit",
7 | platforms: [.iOS(.v12)],
8 | products: [
9 | .library(
10 | name: "TBIconTransitionKit",
11 | targets: ["TBIconTransitionKit"]),
12 | ],
13 | targets: [
14 | .target(
15 | name: "TBIconTransitionKit",
16 | path: "Sources"),
17 | ]
18 | )
19 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # OS X
2 | .DS_Store
3 |
4 | # Xcode
5 | build/
6 | *.pbxuser
7 | !default.pbxuser
8 | *.mode1v3
9 | !default.mode1v3
10 | *.mode2v3
11 | !default.mode2v3
12 | *.perspectivev3
13 | !default.perspectivev3
14 | xcuserdata
15 | *.xccheckout
16 | profile
17 | *.moved-aside
18 | DerivedData
19 | *.hmap
20 | *.ipa
21 | *.xcuserstate
22 |
23 | # CocoaPods
24 | #
25 | # We recommend against adding the Pods directory to your .gitignore. However
26 | # you should judge for yourself, the pros and cons are mentioned at:
27 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control
28 | #
29 | #Pods/
30 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | Copyright (c) 2015 AlexeyBelezeko
3 |
4 | Permission is hereby granted, free of charge, to any person obtaining a copy
5 | of this software and associated documentation files (the "Software"), to deal
6 | in the Software without restriction, including without limitation the rights
7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | copies of the Software, and to permit persons to whom the Software is
9 | furnished to do so, subject to the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be included in
12 | all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 |
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/TBIconTransitionKitExample/TBIconTransitionKitExample/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "platform" : "ios",
6 | "size" : "1024x1024"
7 | },
8 | {
9 | "idiom" : "mac",
10 | "scale" : "1x",
11 | "size" : "16x16"
12 | },
13 | {
14 | "idiom" : "mac",
15 | "scale" : "2x",
16 | "size" : "16x16"
17 | },
18 | {
19 | "idiom" : "mac",
20 | "scale" : "1x",
21 | "size" : "32x32"
22 | },
23 | {
24 | "idiom" : "mac",
25 | "scale" : "2x",
26 | "size" : "32x32"
27 | },
28 | {
29 | "idiom" : "mac",
30 | "scale" : "1x",
31 | "size" : "128x128"
32 | },
33 | {
34 | "idiom" : "mac",
35 | "scale" : "2x",
36 | "size" : "128x128"
37 | },
38 | {
39 | "idiom" : "mac",
40 | "scale" : "1x",
41 | "size" : "256x256"
42 | },
43 | {
44 | "idiom" : "mac",
45 | "scale" : "2x",
46 | "size" : "256x256"
47 | },
48 | {
49 | "idiom" : "mac",
50 | "scale" : "1x",
51 | "size" : "512x512"
52 | },
53 | {
54 | "idiom" : "mac",
55 | "scale" : "2x",
56 | "size" : "512x512"
57 | }
58 | ],
59 | "info" : {
60 | "author" : "xcode",
61 | "version" : 1
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # TBIconTransitionKit
2 |
3 | TBIconTransitionKit is an easy to use icon transition kit that allows to smoothly change from one shape to another.
4 | Build on UIButton with CAShapeLayers It includes a set of the most common navigation icons. Feel free to recolor the them as you like and customise shapes — adjust the line spacing, edit the line width and it's cap.
5 |
6 | [Rewriting project with ChatGPT](https://medium.com/@alexeyybelzeko/exploring-chatgpts-potential-addressing-advanced-programming-challenges-beyond-basic-games-39521ef03299)
7 |
8 | [Animation on dribbble.com](http://drbl.in/poGN)
9 |
10 | 
11 |
12 | Both ways animated transitions:
13 |
14 | - Menu ↔ Arrow
15 | - Menu ↔ Cross
16 | - Cross ↔ Plus
17 | - Plus ↔ Minus
18 |
19 | ## Usage
20 |
21 | To run the example project, clone the repo, and open the TBIconTransitionKitExample project.
22 |
23 | Add AnimatedButton to your SwiftUI view.
24 |
25 | ```swift
26 | import SwiftUI
27 | import TBIconTransitionKit
28 |
29 | struct ContentView: View {
30 | @State private var buttonState: AnimatedButtonState = .menu
31 |
32 | var body: some View {
33 | AnimatedButton(state: .menu, configure: { button in
34 | button.backgroundColor = UIColor(hex: .black)
35 | button.lineColor = .white
36 | }, action: { button in
37 | if button.currentState == .menu {
38 | button.animationTransform(to: .arrow)
39 | } else {
40 | button.animationTransform(to: .menu)
41 | }
42 | })
43 | }
44 | }
45 |
46 | ```
47 |
48 | ### Customize the design
49 |
50 | - `lineHeight`
51 | - `lineWidth`
52 | - `lineSpacing`
53 | - `lineColor`
54 | - `lineCap`
55 |
56 | ## Requirements
57 |
58 | - iOS 13 or higher
59 |
60 | ## Installation
61 |
62 | TBIconTransitionKit can be installed using Swift Package Manager.
63 |
64 | 1. In Xcode, open your project, and select File > Swift Packages > Add Package Dependency.
65 | 2. Enter the repository URL https://github.com/AlexeyBelezeko/TBIconTransitionKit and click Next.
66 | 3. Select the version you'd like to use and click Next.
67 | 4. Finally, click Finish to add the package to your project.
68 |
69 | ## Author
70 |
71 | - [AlexeyBelezeko](https://github.com/AlexeyBelezeko)
72 | - [Oleg Turbaba](https://dribbble.com/turbaba)
73 | - [ChatGPT]
74 |
75 | ## License
76 |
77 | TBIconTransitionKit is available under the MIT license. See the LICENSE file for more info.
78 |
--------------------------------------------------------------------------------
/TBIconTransitionKitExample/TBIconTransitionKitExample/ContentView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentView.swift
3 | // TBIconTransitionKitExample
4 | //
5 | // Created by Aleksei Belezeko on 18.04.2023.
6 | //
7 |
8 | import SwiftUI
9 | import TBIconTransitionKit
10 |
11 | extension UIColor {
12 | convenience init(hex: UInt32) {
13 | let r = CGFloat((hex & 0xFF0000) >> 16) / 255.0
14 | let g = CGFloat((hex & 0x00FF00) >> 8) / 255.0
15 | let b = CGFloat(hex & 0x0000FF) / 255.0
16 | let a = CGFloat(1.0)
17 |
18 | self.init(red: r, green: g, blue: b, alpha: a)
19 | }
20 | }
21 |
22 | struct ContentView: View {
23 | var body: some View {
24 | VStack {
25 | Text("TBIconTransitionKit Version 2.0.0 By Alexey Belezenko and Oleg Turbaba")
26 | .font(.largeTitle)
27 | .padding()
28 | .frame(maxWidth: .infinity, maxHeight: .infinity)
29 | VStack {
30 | HStack {
31 | AnimatedButton(state: .menu, configure: { button in
32 | button.backgroundColor = UIColor(hex: 0xFC2125)
33 | button.lineColor = .white
34 | }, action: { button in
35 | if button.currentState == .menu {
36 | button.animationTransform(to: .arrow)
37 | } else {
38 | button.animationTransform(to: .menu)
39 | }
40 | })
41 | .padding()
42 | .frame(maxWidth: .infinity)
43 |
44 | AnimatedButton(state: .cross, configure: { button in
45 | button.backgroundColor = UIColor(hex: 0x68C4C9)
46 | button.lineColor = .white
47 | }, action: { button in
48 | if button.currentState == .menu {
49 | button.animationTransform(to: .cross)
50 | } else {
51 | button.animationTransform(to: .menu)
52 | }
53 | })
54 | .padding()
55 | .frame(maxWidth: .infinity)
56 | }
57 |
58 | HStack {
59 | AnimatedButton(state: .plus, configure: { button in
60 | button.backgroundColor = UIColor(hex: 0x1E1E22)
61 | button.lineColor = .white
62 | }, action: { button in
63 | if button.currentState == .plus {
64 | button.animationTransform(to: .minus)
65 | } else {
66 | button.animationTransform(to: .plus)
67 | }
68 | })
69 | .padding()
70 | .frame(maxWidth: .infinity)
71 |
72 | AnimatedButton(state: .plus, configure: { button in
73 | button.backgroundColor = UIColor(hex: 0x0F4359)
74 | button.lineColor = .white
75 | }, action: { button in
76 | if button.currentState == .plus {
77 | button.animationTransform(to: .cross)
78 | } else {
79 | button.animationTransform(to: .plus)
80 | }
81 | })
82 | .padding()
83 | .frame(maxWidth: .infinity)
84 | }
85 | }
86 | .frame(maxWidth: .infinity, maxHeight: .infinity)
87 | }
88 | .frame(maxWidth: .infinity, maxHeight: .infinity)
89 | }
90 | }
91 |
92 | struct ContentView_Previews: PreviewProvider {
93 | static var previews: some View {
94 | ContentView()
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/TBIconTransitionKitExample/TBIconTransitionKitExample.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 56;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 9824F8E929EF2C7100BBFE73 /* TBIconTransitionKit in Frameworks */ = {isa = PBXBuildFile; productRef = 9824F8E829EF2C7100BBFE73 /* TBIconTransitionKit */; };
11 | 9893D4AA29EF261D0025D9DF /* TBIconTransitionKitExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9893D4A929EF261D0025D9DF /* TBIconTransitionKitExampleApp.swift */; };
12 | 9893D4AC29EF261D0025D9DF /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9893D4AB29EF261D0025D9DF /* ContentView.swift */; };
13 | 9893D4AE29EF261E0025D9DF /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9893D4AD29EF261E0025D9DF /* Assets.xcassets */; };
14 | 9893D4B229EF261E0025D9DF /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9893D4B129EF261E0025D9DF /* Preview Assets.xcassets */; };
15 | /* End PBXBuildFile section */
16 |
17 | /* Begin PBXFileReference section */
18 | 9824F8E729EF2C6600BBFE73 /* TBIconTransitionKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = TBIconTransitionKit; path = ..; sourceTree = ""; };
19 | 9893D4A629EF261D0025D9DF /* TBIconTransitionKitExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TBIconTransitionKitExample.app; sourceTree = BUILT_PRODUCTS_DIR; };
20 | 9893D4A929EF261D0025D9DF /* TBIconTransitionKitExampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TBIconTransitionKitExampleApp.swift; sourceTree = ""; };
21 | 9893D4AB29EF261D0025D9DF /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
22 | 9893D4AD29EF261E0025D9DF /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
23 | 9893D4AF29EF261E0025D9DF /* TBIconTransitionKitExample.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = TBIconTransitionKitExample.entitlements; sourceTree = ""; };
24 | 9893D4B129EF261E0025D9DF /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
25 | /* End PBXFileReference section */
26 |
27 | /* Begin PBXFrameworksBuildPhase section */
28 | 9893D4A329EF261D0025D9DF /* Frameworks */ = {
29 | isa = PBXFrameworksBuildPhase;
30 | buildActionMask = 2147483647;
31 | files = (
32 | 9824F8E929EF2C7100BBFE73 /* TBIconTransitionKit in Frameworks */,
33 | );
34 | runOnlyForDeploymentPostprocessing = 0;
35 | };
36 | /* End PBXFrameworksBuildPhase section */
37 |
38 | /* Begin PBXGroup section */
39 | 9893D49D29EF261D0025D9DF = {
40 | isa = PBXGroup;
41 | children = (
42 | 9893D4B829EF26E50025D9DF /* Packages */,
43 | 9893D4A829EF261D0025D9DF /* TBIconTransitionKitExample */,
44 | 9893D4A729EF261D0025D9DF /* Products */,
45 | 9893D4BC29EF2AB50025D9DF /* Frameworks */,
46 | );
47 | sourceTree = "";
48 | };
49 | 9893D4A729EF261D0025D9DF /* Products */ = {
50 | isa = PBXGroup;
51 | children = (
52 | 9893D4A629EF261D0025D9DF /* TBIconTransitionKitExample.app */,
53 | );
54 | name = Products;
55 | sourceTree = "";
56 | };
57 | 9893D4A829EF261D0025D9DF /* TBIconTransitionKitExample */ = {
58 | isa = PBXGroup;
59 | children = (
60 | 9893D4A929EF261D0025D9DF /* TBIconTransitionKitExampleApp.swift */,
61 | 9893D4AB29EF261D0025D9DF /* ContentView.swift */,
62 | 9893D4AD29EF261E0025D9DF /* Assets.xcassets */,
63 | 9893D4AF29EF261E0025D9DF /* TBIconTransitionKitExample.entitlements */,
64 | 9893D4B029EF261E0025D9DF /* Preview Content */,
65 | );
66 | path = TBIconTransitionKitExample;
67 | sourceTree = "";
68 | };
69 | 9893D4B029EF261E0025D9DF /* Preview Content */ = {
70 | isa = PBXGroup;
71 | children = (
72 | 9893D4B129EF261E0025D9DF /* Preview Assets.xcassets */,
73 | );
74 | path = "Preview Content";
75 | sourceTree = "";
76 | };
77 | 9893D4B829EF26E50025D9DF /* Packages */ = {
78 | isa = PBXGroup;
79 | children = (
80 | 9824F8E729EF2C6600BBFE73 /* TBIconTransitionKit */,
81 | );
82 | name = Packages;
83 | sourceTree = "";
84 | };
85 | 9893D4BC29EF2AB50025D9DF /* Frameworks */ = {
86 | isa = PBXGroup;
87 | children = (
88 | );
89 | name = Frameworks;
90 | sourceTree = "";
91 | };
92 | /* End PBXGroup section */
93 |
94 | /* Begin PBXNativeTarget section */
95 | 9893D4A529EF261D0025D9DF /* TBIconTransitionKitExample */ = {
96 | isa = PBXNativeTarget;
97 | buildConfigurationList = 9893D4B529EF261E0025D9DF /* Build configuration list for PBXNativeTarget "TBIconTransitionKitExample" */;
98 | buildPhases = (
99 | 9893D4A229EF261D0025D9DF /* Sources */,
100 | 9893D4A329EF261D0025D9DF /* Frameworks */,
101 | 9893D4A429EF261D0025D9DF /* Resources */,
102 | );
103 | buildRules = (
104 | );
105 | dependencies = (
106 | );
107 | name = TBIconTransitionKitExample;
108 | packageProductDependencies = (
109 | 9824F8E829EF2C7100BBFE73 /* TBIconTransitionKit */,
110 | );
111 | productName = TBIconTransitionKitExample;
112 | productReference = 9893D4A629EF261D0025D9DF /* TBIconTransitionKitExample.app */;
113 | productType = "com.apple.product-type.application";
114 | };
115 | /* End PBXNativeTarget section */
116 |
117 | /* Begin PBXProject section */
118 | 9893D49E29EF261D0025D9DF /* Project object */ = {
119 | isa = PBXProject;
120 | attributes = {
121 | BuildIndependentTargetsInParallel = 1;
122 | LastSwiftUpdateCheck = 1430;
123 | LastUpgradeCheck = 1430;
124 | TargetAttributes = {
125 | 9893D4A529EF261D0025D9DF = {
126 | CreatedOnToolsVersion = 14.3;
127 | };
128 | };
129 | };
130 | buildConfigurationList = 9893D4A129EF261D0025D9DF /* Build configuration list for PBXProject "TBIconTransitionKitExample" */;
131 | compatibilityVersion = "Xcode 14.0";
132 | developmentRegion = en;
133 | hasScannedForEncodings = 0;
134 | knownRegions = (
135 | en,
136 | Base,
137 | );
138 | mainGroup = 9893D49D29EF261D0025D9DF;
139 | productRefGroup = 9893D4A729EF261D0025D9DF /* Products */;
140 | projectDirPath = "";
141 | projectRoot = "";
142 | targets = (
143 | 9893D4A529EF261D0025D9DF /* TBIconTransitionKitExample */,
144 | );
145 | };
146 | /* End PBXProject section */
147 |
148 | /* Begin PBXResourcesBuildPhase section */
149 | 9893D4A429EF261D0025D9DF /* Resources */ = {
150 | isa = PBXResourcesBuildPhase;
151 | buildActionMask = 2147483647;
152 | files = (
153 | 9893D4B229EF261E0025D9DF /* Preview Assets.xcassets in Resources */,
154 | 9893D4AE29EF261E0025D9DF /* Assets.xcassets in Resources */,
155 | );
156 | runOnlyForDeploymentPostprocessing = 0;
157 | };
158 | /* End PBXResourcesBuildPhase section */
159 |
160 | /* Begin PBXSourcesBuildPhase section */
161 | 9893D4A229EF261D0025D9DF /* Sources */ = {
162 | isa = PBXSourcesBuildPhase;
163 | buildActionMask = 2147483647;
164 | files = (
165 | 9893D4AC29EF261D0025D9DF /* ContentView.swift in Sources */,
166 | 9893D4AA29EF261D0025D9DF /* TBIconTransitionKitExampleApp.swift in Sources */,
167 | );
168 | runOnlyForDeploymentPostprocessing = 0;
169 | };
170 | /* End PBXSourcesBuildPhase section */
171 |
172 | /* Begin XCBuildConfiguration section */
173 | 9893D4B329EF261E0025D9DF /* Debug */ = {
174 | isa = XCBuildConfiguration;
175 | buildSettings = {
176 | ALWAYS_SEARCH_USER_PATHS = NO;
177 | CLANG_ANALYZER_NONNULL = YES;
178 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
179 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
180 | CLANG_ENABLE_MODULES = YES;
181 | CLANG_ENABLE_OBJC_ARC = YES;
182 | CLANG_ENABLE_OBJC_WEAK = YES;
183 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
184 | CLANG_WARN_BOOL_CONVERSION = YES;
185 | CLANG_WARN_COMMA = YES;
186 | CLANG_WARN_CONSTANT_CONVERSION = YES;
187 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
188 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
189 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
190 | CLANG_WARN_EMPTY_BODY = YES;
191 | CLANG_WARN_ENUM_CONVERSION = YES;
192 | CLANG_WARN_INFINITE_RECURSION = YES;
193 | CLANG_WARN_INT_CONVERSION = YES;
194 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
195 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
196 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
197 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
198 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
199 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
200 | CLANG_WARN_STRICT_PROTOTYPES = YES;
201 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
202 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
203 | CLANG_WARN_UNREACHABLE_CODE = YES;
204 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
205 | COPY_PHASE_STRIP = NO;
206 | DEBUG_INFORMATION_FORMAT = dwarf;
207 | ENABLE_STRICT_OBJC_MSGSEND = YES;
208 | ENABLE_TESTABILITY = YES;
209 | GCC_C_LANGUAGE_STANDARD = gnu11;
210 | GCC_DYNAMIC_NO_PIC = NO;
211 | GCC_NO_COMMON_BLOCKS = YES;
212 | GCC_OPTIMIZATION_LEVEL = 0;
213 | GCC_PREPROCESSOR_DEFINITIONS = (
214 | "DEBUG=1",
215 | "$(inherited)",
216 | );
217 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
218 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
219 | GCC_WARN_UNDECLARED_SELECTOR = YES;
220 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
221 | GCC_WARN_UNUSED_FUNCTION = YES;
222 | GCC_WARN_UNUSED_VARIABLE = YES;
223 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
224 | MTL_FAST_MATH = YES;
225 | ONLY_ACTIVE_ARCH = YES;
226 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
227 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
228 | };
229 | name = Debug;
230 | };
231 | 9893D4B429EF261E0025D9DF /* Release */ = {
232 | isa = XCBuildConfiguration;
233 | buildSettings = {
234 | ALWAYS_SEARCH_USER_PATHS = NO;
235 | CLANG_ANALYZER_NONNULL = YES;
236 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
237 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
238 | CLANG_ENABLE_MODULES = YES;
239 | CLANG_ENABLE_OBJC_ARC = YES;
240 | CLANG_ENABLE_OBJC_WEAK = YES;
241 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
242 | CLANG_WARN_BOOL_CONVERSION = YES;
243 | CLANG_WARN_COMMA = YES;
244 | CLANG_WARN_CONSTANT_CONVERSION = YES;
245 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
246 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
247 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
248 | CLANG_WARN_EMPTY_BODY = YES;
249 | CLANG_WARN_ENUM_CONVERSION = YES;
250 | CLANG_WARN_INFINITE_RECURSION = YES;
251 | CLANG_WARN_INT_CONVERSION = YES;
252 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
253 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
254 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
255 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
256 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
257 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
258 | CLANG_WARN_STRICT_PROTOTYPES = YES;
259 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
260 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
261 | CLANG_WARN_UNREACHABLE_CODE = YES;
262 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
263 | COPY_PHASE_STRIP = NO;
264 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
265 | ENABLE_NS_ASSERTIONS = NO;
266 | ENABLE_STRICT_OBJC_MSGSEND = YES;
267 | GCC_C_LANGUAGE_STANDARD = gnu11;
268 | GCC_NO_COMMON_BLOCKS = YES;
269 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
270 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
271 | GCC_WARN_UNDECLARED_SELECTOR = YES;
272 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
273 | GCC_WARN_UNUSED_FUNCTION = YES;
274 | GCC_WARN_UNUSED_VARIABLE = YES;
275 | MTL_ENABLE_DEBUG_INFO = NO;
276 | MTL_FAST_MATH = YES;
277 | SWIFT_COMPILATION_MODE = wholemodule;
278 | SWIFT_OPTIMIZATION_LEVEL = "-O";
279 | };
280 | name = Release;
281 | };
282 | 9893D4B629EF261E0025D9DF /* Debug */ = {
283 | isa = XCBuildConfiguration;
284 | buildSettings = {
285 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
286 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
287 | CODE_SIGN_ENTITLEMENTS = TBIconTransitionKitExample/TBIconTransitionKitExample.entitlements;
288 | CODE_SIGN_STYLE = Automatic;
289 | CURRENT_PROJECT_VERSION = 1;
290 | DEVELOPMENT_ASSET_PATHS = "\"TBIconTransitionKitExample/Preview Content\"";
291 | ENABLE_PREVIEWS = YES;
292 | GENERATE_INFOPLIST_FILE = YES;
293 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
294 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
295 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
296 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES;
297 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES;
298 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES;
299 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault;
300 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
301 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
302 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
303 | IPHONEOS_DEPLOYMENT_TARGET = 16.4;
304 | LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
305 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
306 | MACOSX_DEPLOYMENT_TARGET = 13.3;
307 | MARKETING_VERSION = 1.0;
308 | PRODUCT_BUNDLE_IDENTIFIER = org.github.TBIconTransitionKitExample;
309 | PRODUCT_NAME = "$(TARGET_NAME)";
310 | SDKROOT = auto;
311 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
312 | SUPPORTS_MACCATALYST = NO;
313 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
314 | SWIFT_EMIT_LOC_STRINGS = YES;
315 | SWIFT_VERSION = 5.0;
316 | TARGETED_DEVICE_FAMILY = "1,2";
317 | };
318 | name = Debug;
319 | };
320 | 9893D4B729EF261E0025D9DF /* Release */ = {
321 | isa = XCBuildConfiguration;
322 | buildSettings = {
323 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
324 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
325 | CODE_SIGN_ENTITLEMENTS = TBIconTransitionKitExample/TBIconTransitionKitExample.entitlements;
326 | CODE_SIGN_STYLE = Automatic;
327 | CURRENT_PROJECT_VERSION = 1;
328 | DEVELOPMENT_ASSET_PATHS = "\"TBIconTransitionKitExample/Preview Content\"";
329 | ENABLE_PREVIEWS = YES;
330 | GENERATE_INFOPLIST_FILE = YES;
331 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
332 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
333 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
334 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES;
335 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES;
336 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES;
337 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault;
338 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
339 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
340 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
341 | IPHONEOS_DEPLOYMENT_TARGET = 16.4;
342 | LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
343 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
344 | MACOSX_DEPLOYMENT_TARGET = 13.3;
345 | MARKETING_VERSION = 1.0;
346 | PRODUCT_BUNDLE_IDENTIFIER = org.github.TBIconTransitionKitExample;
347 | PRODUCT_NAME = "$(TARGET_NAME)";
348 | SDKROOT = auto;
349 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
350 | SUPPORTS_MACCATALYST = NO;
351 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
352 | SWIFT_EMIT_LOC_STRINGS = YES;
353 | SWIFT_VERSION = 5.0;
354 | TARGETED_DEVICE_FAMILY = "1,2";
355 | };
356 | name = Release;
357 | };
358 | /* End XCBuildConfiguration section */
359 |
360 | /* Begin XCConfigurationList section */
361 | 9893D4A129EF261D0025D9DF /* Build configuration list for PBXProject "TBIconTransitionKitExample" */ = {
362 | isa = XCConfigurationList;
363 | buildConfigurations = (
364 | 9893D4B329EF261E0025D9DF /* Debug */,
365 | 9893D4B429EF261E0025D9DF /* Release */,
366 | );
367 | defaultConfigurationIsVisible = 0;
368 | defaultConfigurationName = Release;
369 | };
370 | 9893D4B529EF261E0025D9DF /* Build configuration list for PBXNativeTarget "TBIconTransitionKitExample" */ = {
371 | isa = XCConfigurationList;
372 | buildConfigurations = (
373 | 9893D4B629EF261E0025D9DF /* Debug */,
374 | 9893D4B729EF261E0025D9DF /* Release */,
375 | );
376 | defaultConfigurationIsVisible = 0;
377 | defaultConfigurationName = Release;
378 | };
379 | /* End XCConfigurationList section */
380 |
381 | /* Begin XCSwiftPackageProductDependency section */
382 | 9824F8E829EF2C7100BBFE73 /* TBIconTransitionKit */ = {
383 | isa = XCSwiftPackageProductDependency;
384 | productName = TBIconTransitionKit;
385 | };
386 | /* End XCSwiftPackageProductDependency section */
387 | };
388 | rootObject = 9893D49E29EF261D0025D9DF /* Project object */;
389 | }
390 |
--------------------------------------------------------------------------------
/Sources/TBIconTransitionKit/AnimationButton.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import SwiftUI
3 |
4 | public enum AnimatedButtonState: Int {
5 | case menu
6 | case arrow
7 | case cross
8 | case plus
9 | case minus
10 | }
11 |
12 | /// `AnimatedButton` is a `UIViewRepresentable` wrapper for `TBAnimationButton`.
13 | /// It enables the use of `TBAnimationButton` in SwiftUI, providing an easy way to integrate
14 | /// a customizable button with an animated icon.
15 | ///
16 | /// Wrapped `TBAnimationButton` is a subclass of `UIButton` with an icon drawn with code.
17 | /// The appearance of the icon can be modified by changing the line width, line cap, etc.
18 | @available(iOS 13.0, *)
19 | public struct AnimatedButton: UIViewRepresentable {
20 | let state: AnimatedButtonState
21 | let action: (TBAnimatedButton) -> ()
22 | let configure: (TBAnimatedButton) -> ()
23 |
24 | public func makeCoordinator() -> Coordinator { Coordinator(self) }
25 |
26 | public init(
27 | state: AnimatedButtonState,
28 | configure: @escaping (TBAnimatedButton) -> (),
29 | action: @escaping (TBAnimatedButton) -> ()
30 | ) {
31 | self.state = state
32 | self.action = action
33 | self.configure = configure
34 | }
35 |
36 | public class Coordinator: NSObject {
37 | var parent: AnimatedButton
38 |
39 | init(_ button: AnimatedButton) {
40 | self.parent = button
41 | super.init()
42 | }
43 |
44 | @objc func doAction(_ sender: TBAnimatedButton) {
45 | self.parent.action(sender)
46 | }
47 | }
48 |
49 | public func makeUIView(context: Context) -> TBAnimatedButton {
50 | let button = TBAnimatedButton()
51 | // Assign the properties to the button instance
52 | button.currentState = state
53 | configure(button)
54 | button.addTarget(context.coordinator, action: #selector(Coordinator.doAction(_ :)), for: .touchDown)
55 | return button
56 | }
57 |
58 | public func updateUIView(_ uiView: TBAnimatedButton, context: Context) {}
59 | }
60 |
61 | /// `TBAnimationButton` is a subclass of `UIButton` with an icon. All icons are drawn with code.
62 | /// You can modify the icon appearance by changing the line width, line cap, etc.
63 | /// All icons are built with a hamburger menu transformation.
64 | ///
65 | /// There are 4 animated transforms between states:
66 | /// - Menu and Arrow
67 | /// - Menu and Cross
68 | /// - Cross and Plus
69 | /// - Plus and Minus
70 | ///
71 | /// If you call `animationTransformToState` for other states, they will change without animation.
72 | ///
73 | /// To change the button icon, you should set `currentState` to one of `TBAnimationButtonState`, and call `updateAppearance` method.
74 | public class TBAnimatedButton: UIButton {
75 |
76 | private static let tbScaleForArrow: CGFloat = 0.7
77 | private static let tbAnimationKey: String = "tbAnimationKey"
78 | private static let tbFrameRate: CGFloat = 1.0 / 30.0
79 | private static let tbAnimationFrames: CGFloat = 10.0
80 |
81 | // Layers for lines
82 | private var topLayer = CAShapeLayer()
83 | private var middleLayer = CAShapeLayer()
84 | private var bottomLayer = CAShapeLayer()
85 |
86 | private var needsToUpdateAppearance = false
87 |
88 | public var lineHeight: CGFloat = 2.0 {
89 | didSet {
90 | needsToUpdateAppearance = true
91 | }
92 | }
93 |
94 | public var lineWidth: CGFloat = 30.0 {
95 | didSet {
96 | needsToUpdateAppearance = true
97 | }
98 | }
99 |
100 | public var lineSpacing: CGFloat = 8.0 {
101 | didSet {
102 | needsToUpdateAppearance = true
103 | }
104 | }
105 |
106 | public var lineColor: UIColor = .black {
107 | didSet {
108 | needsToUpdateAppearance = true
109 | }
110 | }
111 |
112 | public var lineCap: CAShapeLayerLineCap = .butt {
113 | didSet {
114 | needsToUpdateAppearance = true
115 | }
116 | }
117 |
118 | private var innerState: AnimatedButtonState = .menu
119 | public var currentState: AnimatedButtonState {
120 | get {
121 | return innerState
122 | }
123 | set {
124 | innerState = newValue
125 | transformTo(state: currentState)
126 | }
127 | }
128 |
129 | override init(frame: CGRect) {
130 | super.init(frame: frame)
131 | needsToUpdateAppearance = true
132 | }
133 |
134 | required init?(coder aDecoder: NSCoder) {
135 | super.init(coder: aDecoder)
136 | needsToUpdateAppearance = true
137 | }
138 |
139 | public override func layoutSubviews() {
140 | super.layoutSubviews()
141 | if needsToUpdateAppearance {
142 | updateAppearance()
143 | }
144 | }
145 |
146 | func updateAppearance() {
147 | needsToUpdateAppearance = false
148 | topLayer.removeFromSuperlayer()
149 | middleLayer.removeFromSuperlayer()
150 | bottomLayer.removeFromSuperlayer()
151 |
152 | let x = bounds.width / 2.0
153 | let heightDiff = lineHeight + lineSpacing
154 | var y = bounds.height / 2.0 - heightDiff
155 |
156 | topLayer = createLayer()
157 | topLayer.position = CGPoint(x: x, y: y)
158 | y += heightDiff
159 |
160 | middleLayer = createLayer()
161 | middleLayer.position = CGPoint(x: x, y: y)
162 | y += heightDiff
163 |
164 | bottomLayer = createLayer()
165 | bottomLayer.position = CGPoint(x: x, y: y)
166 | transformTo(state: currentState)
167 | }
168 |
169 | public func transformTo(state: AnimatedButtonState) {
170 | var transform: CATransform3D
171 | switch state {
172 | case .arrow:
173 | topLayer.transform = arrowLineTransform(line: topLayer)
174 | middleLayer.transform = arrowLineTransform(line: middleLayer)
175 | bottomLayer.transform = arrowLineTransform(line: bottomLayer)
176 | case .cross:
177 | transform = CATransform3DMakeTranslation(0.0, middleLayer.position.y - topLayer.position.y, 0.0)
178 | topLayer.transform = CATransform3DRotate(transform, .pi / 4, 0.0, 0.0, 1.0)
179 | middleLayer.transform = CATransform3DMakeScale(0.0, 0.0, 0.0)
180 | transform = CATransform3DMakeTranslation(0.0, middleLayer.position.y - bottomLayer.position.y, 0.0)
181 | bottomLayer.transform = CATransform3DRotate(transform, -.pi / 4, 0.0, 0.0, 1.0)
182 | case .minus:
183 | topLayer.transform = CATransform3DMakeTranslation(0.0, middleLayer.position.y - topLayer.position.y, 0.0)
184 | middleLayer.transform = CATransform3DMakeScale(0.0, 0.0, 0.0)
185 | bottomLayer.transform = CATransform3DMakeScale(0.0, 0.0, 0.0)
186 | case .plus:
187 | transform = CATransform3DMakeTranslation(0.0, middleLayer.position.y - topLayer.position.y, 0.0)
188 | topLayer.transform = transform
189 | middleLayer.transform = CATransform3DMakeScale(0.0, 0.0, 0.0)
190 | transform = CATransform3DMakeTranslation(0.0, middleLayer.position.y - bottomLayer.position.y, 0.0)
191 | bottomLayer.transform = CATransform3DRotate(transform, -.pi / 2, 0.0, 0.0, 1.0)
192 | default:
193 | topLayer.transform = CATransform3DIdentity
194 | middleLayer.transform = CATransform3DIdentity
195 | bottomLayer.transform = CATransform3DIdentity
196 | }
197 | innerState = state
198 | }
199 |
200 | func arrowLineTransform(line: CALayer) -> CATransform3D {
201 | var transform: CATransform3D
202 | if line == middleLayer {
203 | let middleLineXScale = lineHeight / lineWidth
204 | transform = CATransform3DMakeScale(1.0 - middleLineXScale, 1.0, 1.0)
205 | transform = CATransform3DTranslate(transform, lineWidth * middleLineXScale / 2.0, 0.0, 0.0)
206 | return transform
207 | }
208 | let lineMult: CGFloat = line == topLayer ? 1.0 : -1.0
209 | var yShift: CGFloat = 0.0
210 | if lineCap == .butt {
211 | yShift = sqrt(2) * lineHeight / 4.0
212 | }
213 | let lineShift = lineWidth * (1.0 - TBAnimatedButton.tbScaleForArrow) / 2.0
214 | transform = CATransform3DMakeTranslation(-lineShift, middleLayer.position.y - line.position.y + yShift * lineMult, 0.0)
215 | let xTransform = lineWidth / 2.0 - lineShift
216 | transform = CATransform3DTranslate(transform, -xTransform, 0 , 0.0)
217 | transform = CATransform3DRotate(transform, .pi / 4 * lineMult, 0.0, 0.0, -1.0)
218 | transform = CATransform3DTranslate(transform, xTransform, 0, 0.0)
219 | transform = CATransform3DScale(transform, TBAnimatedButton.tbScaleForArrow, 1.0, 1.0)
220 | return transform
221 | }
222 |
223 | public func animationTransform(to state: AnimatedButtonState) {
224 | if innerState == state {
225 | return
226 | }
227 | var findAnimationForTransition = false
228 | switch innerState {
229 | case .arrow:
230 | if state == .menu {
231 | findAnimationForTransition = true
232 | animationTransitionFromMenuToArrow(reverse: true)
233 | }
234 | case .cross:
235 | if state == .menu {
236 | findAnimationForTransition = true
237 | animationTransitionFromMenuToCross(reverse: true)
238 | } else if state == .plus {
239 | findAnimationForTransition = true
240 | animationTransitionFromCrossToPlus(reverse: false)
241 | }
242 | case .minus:
243 | if state == .plus {
244 | findAnimationForTransition = true
245 | animationTransitionFromPlusToMinus(reverse: true)
246 | }
247 | case .plus:
248 | if state == .cross {
249 | findAnimationForTransition = true
250 | animationTransitionFromCrossToPlus(reverse: true)
251 | } else if state == .minus {
252 | findAnimationForTransition = true
253 | animationTransitionFromPlusToMinus(reverse: false)
254 | }
255 | default:
256 | // Default state is menu
257 | if state == .arrow {
258 | findAnimationForTransition = true
259 | animationTransitionFromMenuToArrow(reverse: false)
260 | } else if state == .cross {
261 | findAnimationForTransition = true
262 | animationTransitionFromMenuToCross(reverse: false)
263 | }
264 | }
265 | if !findAnimationForTransition {
266 | print("Can't find animation transition for these states!")
267 | transformTo(state: state)
268 | } else {
269 | innerState = state
270 | }
271 | }
272 |
273 | // MARK: - From menu to arrow
274 |
275 | func animationTransitionFromMenuToArrow(reverse: Bool) {
276 | let times = [0.0, 0.5, 0.5, 1.0]
277 |
278 | let values = fromMenuToArrowAnimationValues(line: topLayer, reverse: reverse)
279 | let topAnimation = createKeyFrameAnimation()
280 | topAnimation.keyTimes = times.map { NSNumber(value: $0) }
281 | topAnimation.values = values
282 |
283 | let bottomValues = fromMenuToArrowAnimationValues(line: bottomLayer, reverse: reverse)
284 | let bottomAnimation = createKeyFrameAnimation()
285 | bottomAnimation.keyTimes = times.map { NSNumber(value: $0) }
286 | bottomAnimation.values = bottomValues
287 |
288 | let middleTransform = arrowLineTransform(line: middleLayer)
289 | var middleValues = [
290 | NSValue(caTransform3D: CATransform3DIdentity),
291 | NSValue(caTransform3D: CATransform3DIdentity),
292 | NSValue(caTransform3D: middleTransform),
293 | NSValue(caTransform3D: middleTransform)
294 | ]
295 | if reverse {
296 | middleValues = middleValues.reversed()
297 | }
298 | let middleTimes = [0.0, 0.4, 0.4, 1.0]
299 | let middleAnimation = createKeyFrameAnimation()
300 | middleAnimation.keyTimes = middleTimes.map { NSNumber(value: $0) }
301 | middleAnimation.values = middleValues
302 | middleLayer.add(middleAnimation, forKey: TBAnimatedButton.tbAnimationKey)
303 | topLayer.add(topAnimation, forKey: TBAnimatedButton.tbAnimationKey)
304 | bottomLayer.add(bottomAnimation, forKey: TBAnimatedButton.tbAnimationKey)
305 | }
306 |
307 | func fromMenuToArrowAnimationValues(line: CALayer, reverse: Bool) -> [NSValue] {
308 | var values = [NSValue]()
309 |
310 | let lineMult: CGFloat = line == topLayer ? 1.0 : -1.0
311 | let yTransform = middleLayer.position.y - line.position.y
312 | var yShift: CGFloat = 0.0
313 | if lineCap == .butt {
314 | yShift = sqrt(2.0) * lineHeight / 4.0
315 | }
316 |
317 | var transform = CATransform3DIdentity
318 | values.append(NSValue(caTransform3D: transform))
319 |
320 | transform = CATransform3DTranslate(transform, 0.0, yTransform, 0.0)
321 | values.append(NSValue(caTransform3D: transform))
322 |
323 | let lineShift = lineWidth * (1.0 - TBAnimatedButton.tbScaleForArrow) / 2.0
324 | let scaleTransform = CATransform3DScale(transform, TBAnimatedButton.tbScaleForArrow, 1.0, 1.0)
325 | let translatedScaleTransform = CATransform3DTranslate(scaleTransform, -lineShift, 0.0, 0.0)
326 | values.append(NSValue(caTransform3D: translatedScaleTransform))
327 |
328 | transform = CATransform3DTranslate(transform, -lineShift, 0.0, 0.0)
329 | let xTransform = lineWidth / 2.0 - lineShift
330 |
331 | transform = CATransform3DTranslate(transform, -xTransform, 0.0, 0.0)
332 | transform = CATransform3DRotate(transform, CGFloat.pi / 4 * lineMult, 0.0, 0.0, -1.0)
333 | transform = CATransform3DTranslate(transform, xTransform, 0.0, 0.0)
334 |
335 | transform = CATransform3DScale(transform, TBAnimatedButton.tbScaleForArrow, 1.0, 1.0)
336 | transform = CATransform3DTranslate(transform, 0.0, yShift * lineMult, 0.0)
337 | values.append(NSValue(caTransform3D: transform))
338 |
339 | if reverse {
340 | values.reverse()
341 | }
342 | return values
343 | }
344 |
345 | // MARK: - From menu to cross
346 |
347 | func animationTransitionFromMenuToCross(reverse: Bool) {
348 | let times: [NSNumber] = [0.0, 0.5, 1.0]
349 |
350 | var values = fromMenuToCrossAnimationValues(line: topLayer, reverse: reverse)
351 | let topAnimation = createKeyFrameAnimation()
352 | topAnimation.keyTimes = times
353 | topAnimation.values = values
354 |
355 | values = fromMenuToCrossAnimationValues(line: bottomLayer, reverse: reverse)
356 | let bottomAnimation = createKeyFrameAnimation()
357 | bottomAnimation.keyTimes = times
358 | bottomAnimation.values = values
359 |
360 | let middleTransform = CATransform3DMakeScale(0.0, 0.0, 0.0)
361 | values = [NSValue(caTransform3D: CATransform3DIdentity),
362 | NSValue(caTransform3D: CATransform3DIdentity),
363 | NSValue(caTransform3D: middleTransform),
364 | NSValue(caTransform3D: middleTransform)]
365 |
366 | if reverse {
367 | values.reverse()
368 | }
369 |
370 | let middleTimes: [NSNumber] = [0.0, 0.5, 0.5, 1.0]
371 | let middleAnimation = createKeyFrameAnimation()
372 | middleAnimation.keyTimes = middleTimes
373 | middleAnimation.values = values
374 | middleLayer.add(middleAnimation, forKey: TBAnimatedButton.tbAnimationKey)
375 | topLayer.add(topAnimation, forKey: TBAnimatedButton.tbAnimationKey)
376 | bottomLayer.add(bottomAnimation, forKey: TBAnimatedButton.tbAnimationKey)
377 | }
378 |
379 | func fromMenuToCrossAnimationValues(line: CALayer, reverse: Bool) -> [NSValue] {
380 | var values: [NSValue] = []
381 | let lineMult: CGFloat = line == topLayer ? 1.0 : -1.0
382 | let yTransform: CGFloat = middleLayer.position.y - line.position.y
383 |
384 | var transform = CATransform3DIdentity
385 | values.append(NSValue(caTransform3D: transform))
386 | transform = CATransform3DTranslate(transform, 0, yTransform, 0.0)
387 | values.append(NSValue(caTransform3D: transform))
388 |
389 | transform = CATransform3DRotate(transform, .pi / 4 * lineMult, 0.0, 0.0, 1.0)
390 | values.append(NSValue(caTransform3D: transform))
391 |
392 | if reverse {
393 | values.reverse()
394 | }
395 |
396 | return values
397 | }
398 |
399 | // MARK: - From cross to plus
400 |
401 | func animationTransitionFromCrossToPlus(reverse: Bool) {
402 | let times: [NSNumber] = reverse ? [1.0, 0.0] : [0.0, 1.0]
403 |
404 | var transform = CATransform3DMakeTranslation(0.0, middleLayer.position.y - topLayer.position.y, 0.0)
405 | transform = CATransform3DRotate(transform, .pi / 4, 0.0, 0.0, 1.0)
406 | let values: [NSValue] = [
407 | NSValue(caTransform3D: transform),
408 | NSValue(caTransform3D: CATransform3DRotate(transform, .pi / 2 + .pi / 4, 0.0, 0.0, 1.0))
409 | ]
410 | let topAnimation = createKeyFrameAnimation()
411 | topAnimation.keyTimes = times
412 | topAnimation.values = values
413 |
414 | transform = CATransform3DMakeTranslation(0.0, middleLayer.position.y - bottomLayer.position.y, 0.0)
415 | transform = CATransform3DRotate(transform, -.pi / 4, 0.0, 0.0, 1.0)
416 | let bottomValues: [NSValue] = [
417 | NSValue(caTransform3D: transform),
418 | NSValue(caTransform3D: CATransform3DRotate(transform, .pi / 2 + .pi / 4, 0.0, 0.0, 1.0))
419 | ]
420 | let bottomAnimation = createKeyFrameAnimation()
421 | bottomAnimation.keyTimes = times
422 | bottomAnimation.values = bottomValues
423 |
424 | topLayer.add(topAnimation, forKey: TBAnimatedButton.tbAnimationKey)
425 | bottomLayer.add(bottomAnimation, forKey: TBAnimatedButton.tbAnimationKey)
426 | }
427 |
428 | // MARK: - From plus to minus
429 |
430 | func animationTransitionFromPlusToMinus(reverse: Bool) {
431 | let times: [NSNumber] = reverse ? [1.0, 0.0] : [0.0, 1.0]
432 |
433 | var transform = CATransform3DMakeTranslation(0.0, middleLayer.position.y - topLayer.position.y, 0.0)
434 | let values: [NSValue] = [
435 | NSValue(caTransform3D: transform),
436 | NSValue(caTransform3D: CATransform3DRotate(transform, -.pi, 0.0, 0.0, 1.0))
437 | ]
438 | let topAnimation = createKeyFrameAnimation()
439 | topAnimation.keyTimes = times
440 | topAnimation.values = values
441 |
442 | transform = CATransform3DMakeTranslation(0.0, middleLayer.position.y - bottomLayer.position.y, 0.0)
443 | transform = CATransform3DRotate(transform, -.pi / 2, 0.0, 0.0, 1.0)
444 | let bottomValues: [NSValue] = [
445 | NSValue(caTransform3D: transform),
446 | NSValue(caTransform3D: CATransform3DRotate(transform, -.pi / 2, 0.0, 0.0, 1.0))
447 | ]
448 | let bottomAnimation = createKeyFrameAnimation()
449 | bottomAnimation.keyTimes = times
450 | bottomAnimation.values = bottomValues
451 |
452 | topLayer.add(topAnimation, forKey: TBAnimatedButton.tbAnimationKey)
453 | bottomLayer.add(bottomAnimation, forKey: TBAnimatedButton.tbAnimationKey)
454 | }
455 |
456 | // MARK: - Helpers
457 |
458 | func createLayer() -> CAShapeLayer {
459 | let layer = CAShapeLayer()
460 |
461 | let path = UIBezierPath()
462 | path.move(to: CGPoint(x: 0, y: 0))
463 | path.addLine(to: CGPoint(x: lineWidth, y: 0))
464 |
465 | layer.path = path.cgPath
466 | layer.lineWidth = lineHeight
467 | layer.strokeColor = lineColor.cgColor
468 | layer.lineCap = lineCap
469 |
470 | let bound = CGPath(__byStroking: layer.path!,
471 | transform: nil,
472 | lineWidth: layer.lineWidth,
473 | lineCap: .butt,
474 | lineJoin: .miter,
475 | miterLimit: layer.miterLimit)
476 | layer.bounds = bound!.boundingBox
477 | self.layer.addSublayer(layer)
478 |
479 | return layer
480 | }
481 |
482 | func createKeyFrameAnimation() -> CAKeyframeAnimation {
483 | let animation = CAKeyframeAnimation(keyPath: "transform")
484 | animation.duration = TBAnimatedButton.tbFrameRate * TBAnimatedButton.tbAnimationFrames
485 | animation.isRemovedOnCompletion = false // Keep changes
486 | animation.fillMode = .forwards // Keep changes
487 | // Custom timing function for really smooth =)
488 | animation.timingFunction = CAMediaTimingFunction(controlPoints: 0.60, 0.00, 0.40, 1.00)
489 |
490 | return animation
491 | }
492 | }
493 |
--------------------------------------------------------------------------------