├── Resources
└── LongPressRecordButton.gif
├── .gitignore
├── Example
├── LongPressRecordButtonSample
│ ├── Assets.xcassets
│ │ └── AppIcon.appiconset
│ │ │ └── Contents.json
│ ├── Info.plist
│ ├── AppDelegate.swift
│ ├── ViewController.swift
│ └── Base.lproj
│ │ ├── LaunchScreen.storyboard
│ │ └── Main.storyboard
└── LongPressRecordButtonSample.xcodeproj
│ └── project.pbxproj
├── LongPressRecordButton.podspec
├── LICENSE
├── README.md
└── LongPressRecordButton
└── LongPressRecordButton.swift
/Resources/LongPressRecordButton.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mkoehnke/LongPressRecordButton/HEAD/Resources/LongPressRecordButton.gif
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | build/*
3 | *.pbxuser
4 | !default.pbxuser
5 | *.mode1v3
6 | !default.mode1v3
7 | *.mode2v3
8 | !default.mode2v3
9 | *.perspectivev3
10 | !default.perspectivev3
11 | *.xcworkspace
12 | !default.xcworkspace
13 | xcuserdata
14 | profile
15 | *.moved-aside
16 | Pods/
17 | Podfile.lock
18 |
19 | .DS_Store
20 |
--------------------------------------------------------------------------------
/Example/LongPressRecordButtonSample/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "size" : "29x29",
6 | "scale" : "2x"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "size" : "29x29",
11 | "scale" : "3x"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "size" : "40x40",
16 | "scale" : "2x"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "size" : "40x40",
21 | "scale" : "3x"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "size" : "60x60",
26 | "scale" : "2x"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "size" : "60x60",
31 | "scale" : "3x"
32 | }
33 | ],
34 | "info" : {
35 | "version" : 1,
36 | "author" : "xcode"
37 | }
38 | }
--------------------------------------------------------------------------------
/LongPressRecordButton.podspec:
--------------------------------------------------------------------------------
1 | Pod::Spec.new do |s|
2 |
3 | s.name = "LongPressRecordButton"
4 | s.version = "1.5.0"
5 | s.summary = "Simple and easy-to-use record button for iOS, that enforces a long press, similar to Instagram"
6 |
7 | s.description = <<-DESC
8 | Simple and easy-to-use record button for iOS, that enforces a long press (and shows a tooltip when short-pressed)
9 | similar to the Instagram app.
10 | DESC
11 |
12 | s.homepage = "https://github.com/mkoehnke/LongPressRecordButton"
13 |
14 | s.license = { :type => 'MIT', :file => 'LICENSE' }
15 |
16 | s.author = "Mathias Köhnke"
17 |
18 | s.platform = :ios, "9.0"
19 |
20 | s.source = { :git => "https://github.com/mkoehnke/LongPressRecordButton.git", :tag => s.version.to_s }
21 |
22 | s.source_files = "LongPressRecordButton", "LongPressRecordButton/**/*.{swift}"
23 | s.exclude_files = "Classes/Exclude"
24 |
25 | s.requires_arc = true
26 |
27 | end
28 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Mathias Köhnke
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/Example/LongPressRecordButtonSample/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | APPL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1
23 | LSRequiresIPhoneOS
24 |
25 | UILaunchStoryboardName
26 | LaunchScreen
27 | UIMainStoryboardFile
28 | Main
29 | UIRequiredDeviceCapabilities
30 |
31 | armv7
32 |
33 | UIStatusBarHidden
34 |
35 | UIStatusBarStyle
36 | UIStatusBarStyleLightContent
37 | UISupportedInterfaceOrientations
38 |
39 | UIInterfaceOrientationPortrait
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # LongPressRecordButton
2 |
3 | [](https://twitter.com/mkoehnke)
4 | [](http://cocoadocs.org/docsets/LongPressRecordButton)
5 | [](http://cocoadocs.org/docsets/LongPressRecordButton)
6 | [](http://cocoadocs.org/docsets/LongPressRecordButton)
7 |
8 |
9 | Simple and easy-to-use record button for iOS, that enforces a long press (and shows a tooltip when short-pressed) similar to the **Instagram** app. _Take a look at the sample project for further information._
10 |
11 |
12 |
13 | # Installation
14 |
15 | ## CocoaPods
16 | [CocoaPods](http://cocoapods.org) is a dependency manager for Cocoa projects. You can install it with the following command:
17 |
18 | ```bash
19 | $ gem install cocoapods
20 | ```
21 |
22 | To integrate the LongPressRecordButton into your Xcode project using CocoaPods, specify it in your `Podfile`:
23 |
24 | ```ruby
25 | source 'https://github.com/CocoaPods/Specs.git'
26 | platform :ios, '9.0'
27 | use_frameworks!
28 |
29 | pod 'LongPressRecordButton'
30 | ```
31 |
32 | Then, run the following command:
33 |
34 | ```bash
35 | $ pod install
36 | ```
37 |
38 | ## Manually
39 | Copy the **LongPressRecordButton.swift** file to your Swift project, add it to a target and you're good to go.
40 |
41 | # Usage
42 | The easiest way to get started is to add the LongPressRecordButton as a custom view in your Storyboard. The appearance can be easily customized using the __Attributes Inspector__ in the _Xcode Utilities area_.
43 |
44 | # Author
45 | Mathias Köhnke [@mkoehnke](http://twitter.com/mkoehnke)
46 |
47 | # License
48 | LongPressRecordButton is available under the MIT license. See the LICENSE file for more info.
49 |
50 | # Recent Changes
51 | The release notes can be found [here](https://github.com/mkoehnke/LongPressRecordButton/releases).
52 |
--------------------------------------------------------------------------------
/Example/LongPressRecordButtonSample/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | //
4 | // Copyright (c) 2015 Mathias Koehnke (http://www.mathiaskoehnke.com)
5 | //
6 | // Permission is hereby granted, free of charge, to any person obtaining a copy
7 | // of this software and associated documentation files (the "Software"), to deal
8 | // in the Software without restriction, including without limitation the rights
9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | // copies of the Software, and to permit persons to whom the Software is
11 | // furnished to do so, subject to the following conditions:
12 | //
13 | // The above copyright notice and this permission notice shall be included in
14 | // all copies or substantial portions of the Software.
15 | //
16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | // THE SOFTWARE.
23 |
24 | import UIKit
25 |
26 | @UIApplicationMain
27 | class AppDelegate: UIResponder, UIApplicationDelegate {
28 |
29 | var window: UIWindow?
30 |
31 |
32 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
33 | // Override point for customization after application launch.
34 | return true
35 | }
36 |
37 | func applicationWillResignActive(_ application: UIApplication) {
38 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
39 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
40 | }
41 |
42 | func applicationDidEnterBackground(_ application: UIApplication) {
43 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
44 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
45 | }
46 |
47 | func applicationWillEnterForeground(_ application: UIApplication) {
48 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background.
49 | }
50 |
51 | func applicationDidBecomeActive(_ application: UIApplication) {
52 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
53 | }
54 |
55 | func applicationWillTerminate(_ application: UIApplication) {
56 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
57 | }
58 |
59 |
60 | }
61 |
62 |
--------------------------------------------------------------------------------
/Example/LongPressRecordButtonSample/ViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController.swift
3 | //
4 | // Copyright (c) 2015 Mathias Koehnke (http://www.mathiaskoehnke.com)
5 | //
6 | // Permission is hereby granted, free of charge, to any person obtaining a copy
7 | // of this software and associated documentation files (the "Software"), to deal
8 | // in the Software without restriction, including without limitation the rights
9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | // copies of the Software, and to permit persons to whom the Software is
11 | // furnished to do so, subject to the following conditions:
12 | //
13 | // The above copyright notice and this permission notice shall be included in
14 | // all copies or substantial portions of the Software.
15 | //
16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | // THE SOFTWARE.
23 |
24 |
25 | import UIKit
26 |
27 | class ViewController: UIViewController, LongPressRecordButtonDelegate {
28 |
29 | @IBOutlet weak var recordButton : LongPressRecordButton?
30 | @IBOutlet weak var progressView : UIProgressView?
31 |
32 | let duration : Double = 5.0
33 | var progress : Double = 0.0
34 | var startTime : CFTimeInterval?
35 |
36 | lazy var displayLink : CADisplayLink? = {
37 | var instance = CADisplayLink(target: self, selector: #selector(ViewController.animateProgress(_:)))
38 | instance.isPaused = true
39 | instance.add(to: RunLoop.main, forMode: RunLoopMode.commonModes)
40 | return instance
41 | }()
42 |
43 | override func viewDidLoad() {
44 | super.viewDidLoad()
45 | recordButton?.delegate = self
46 | setupDisplayLink()
47 | }
48 |
49 | override var preferredStatusBarStyle : UIStatusBarStyle {
50 | return UIStatusBarStyle.lightContent
51 | }
52 |
53 |
54 | // MARK: LongPressRecordButton Delegate
55 |
56 | func longPressRecordButtonDidStartLongPress(_ button: LongPressRecordButton) {
57 | startTime = CACurrentMediaTime();
58 | displayLink?.isPaused = false
59 | }
60 |
61 | func longPressRecordButtonDidStopLongPress(_ button: LongPressRecordButton) {
62 | displayLink?.isPaused = true
63 | }
64 |
65 | // MARK: DisplayLink
66 |
67 | fileprivate func setupDisplayLink() {
68 | progress = 0.0
69 | startTime = CACurrentMediaTime();
70 | displayLink?.isPaused = true
71 | progressView?.progress = Float(progress)
72 | }
73 |
74 | @objc fileprivate func animateProgress(_ displayLink : CADisplayLink) {
75 | if (progress > duration) {
76 | setupDisplayLink()
77 | return
78 | }
79 |
80 | if let startTime = startTime {
81 | let elapsedTime = CACurrentMediaTime() - startTime
82 | self.progress += elapsedTime
83 | self.startTime = CACurrentMediaTime()
84 | self.progressView?.progress = Float(self.progress / self.duration)
85 | }
86 | }
87 | }
88 |
89 |
--------------------------------------------------------------------------------
/Example/LongPressRecordButtonSample/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 |
52 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
--------------------------------------------------------------------------------
/Example/LongPressRecordButtonSample/Base.lproj/Main.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 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
54 |
59 |
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 |
--------------------------------------------------------------------------------
/Example/LongPressRecordButtonSample.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 46;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | BF369CF51BE6BAD200E88DDC /* LongPressRecordButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF369CF41BE6BAD200E88DDC /* LongPressRecordButton.swift */; };
11 | BFD64F431BE00E6C00C41517 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD64F421BE00E6C00C41517 /* AppDelegate.swift */; };
12 | BFD64F451BE00E6C00C41517 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD64F441BE00E6C00C41517 /* ViewController.swift */; };
13 | BFD64F481BE00E6C00C41517 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BFD64F461BE00E6C00C41517 /* Main.storyboard */; };
14 | BFD64F4A1BE00E6C00C41517 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = BFD64F491BE00E6C00C41517 /* Assets.xcassets */; };
15 | BFD64F4D1BE00E6C00C41517 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BFD64F4B1BE00E6C00C41517 /* LaunchScreen.storyboard */; };
16 | /* End PBXBuildFile section */
17 |
18 | /* Begin PBXFileReference section */
19 | BF369CF41BE6BAD200E88DDC /* LongPressRecordButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LongPressRecordButton.swift; sourceTree = ""; };
20 | BFD64F3F1BE00E6C00C41517 /* LongPressRecordButtonSample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = LongPressRecordButtonSample.app; sourceTree = BUILT_PRODUCTS_DIR; };
21 | BFD64F421BE00E6C00C41517 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
22 | BFD64F441BE00E6C00C41517 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; };
23 | BFD64F471BE00E6C00C41517 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
24 | BFD64F491BE00E6C00C41517 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
25 | BFD64F4C1BE00E6C00C41517 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
26 | BFD64F4E1BE00E6C00C41517 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
27 | /* End PBXFileReference section */
28 |
29 | /* Begin PBXFrameworksBuildPhase section */
30 | BFD64F3C1BE00E6C00C41517 /* Frameworks */ = {
31 | isa = PBXFrameworksBuildPhase;
32 | buildActionMask = 2147483647;
33 | files = (
34 | );
35 | runOnlyForDeploymentPostprocessing = 0;
36 | };
37 | /* End PBXFrameworksBuildPhase section */
38 |
39 | /* Begin PBXGroup section */
40 | BF369CF31BE6BAD200E88DDC /* LongPressRecordButton */ = {
41 | isa = PBXGroup;
42 | children = (
43 | BF369CF41BE6BAD200E88DDC /* LongPressRecordButton.swift */,
44 | );
45 | name = LongPressRecordButton;
46 | path = ../../LongPressRecordButton;
47 | sourceTree = "";
48 | };
49 | BFD64F361BE00E6B00C41517 = {
50 | isa = PBXGroup;
51 | children = (
52 | BFD64F411BE00E6C00C41517 /* LongPressRecordButtonSample */,
53 | BFD64F401BE00E6C00C41517 /* Products */,
54 | );
55 | sourceTree = "";
56 | };
57 | BFD64F401BE00E6C00C41517 /* Products */ = {
58 | isa = PBXGroup;
59 | children = (
60 | BFD64F3F1BE00E6C00C41517 /* LongPressRecordButtonSample.app */,
61 | );
62 | name = Products;
63 | sourceTree = "";
64 | };
65 | BFD64F411BE00E6C00C41517 /* LongPressRecordButtonSample */ = {
66 | isa = PBXGroup;
67 | children = (
68 | BF369CF31BE6BAD200E88DDC /* LongPressRecordButton */,
69 | BFD64F421BE00E6C00C41517 /* AppDelegate.swift */,
70 | BFD64F441BE00E6C00C41517 /* ViewController.swift */,
71 | BFD64F461BE00E6C00C41517 /* Main.storyboard */,
72 | BFD64F491BE00E6C00C41517 /* Assets.xcassets */,
73 | BFD64F4B1BE00E6C00C41517 /* LaunchScreen.storyboard */,
74 | BFD64F4E1BE00E6C00C41517 /* Info.plist */,
75 | );
76 | path = LongPressRecordButtonSample;
77 | sourceTree = "";
78 | };
79 | /* End PBXGroup section */
80 |
81 | /* Begin PBXNativeTarget section */
82 | BFD64F3E1BE00E6C00C41517 /* LongPressRecordButtonSample */ = {
83 | isa = PBXNativeTarget;
84 | buildConfigurationList = BFD64F511BE00E6C00C41517 /* Build configuration list for PBXNativeTarget "LongPressRecordButtonSample" */;
85 | buildPhases = (
86 | BFD64F3B1BE00E6C00C41517 /* Sources */,
87 | BFD64F3C1BE00E6C00C41517 /* Frameworks */,
88 | BFD64F3D1BE00E6C00C41517 /* Resources */,
89 | );
90 | buildRules = (
91 | );
92 | dependencies = (
93 | );
94 | name = LongPressRecordButtonSample;
95 | productName = RecordButtonSample;
96 | productReference = BFD64F3F1BE00E6C00C41517 /* LongPressRecordButtonSample.app */;
97 | productType = "com.apple.product-type.application";
98 | };
99 | /* End PBXNativeTarget section */
100 |
101 | /* Begin PBXProject section */
102 | BFD64F371BE00E6B00C41517 /* Project object */ = {
103 | isa = PBXProject;
104 | attributes = {
105 | LastSwiftUpdateCheck = 0710;
106 | LastUpgradeCheck = 0800;
107 | ORGANIZATIONNAME = "Mathias Köhnke";
108 | TargetAttributes = {
109 | BFD64F3E1BE00E6C00C41517 = {
110 | CreatedOnToolsVersion = 7.1;
111 | LastSwiftMigration = 0800;
112 | };
113 | };
114 | };
115 | buildConfigurationList = BFD64F3A1BE00E6B00C41517 /* Build configuration list for PBXProject "LongPressRecordButtonSample" */;
116 | compatibilityVersion = "Xcode 3.2";
117 | developmentRegion = English;
118 | hasScannedForEncodings = 0;
119 | knownRegions = (
120 | en,
121 | Base,
122 | );
123 | mainGroup = BFD64F361BE00E6B00C41517;
124 | productRefGroup = BFD64F401BE00E6C00C41517 /* Products */;
125 | projectDirPath = "";
126 | projectRoot = "";
127 | targets = (
128 | BFD64F3E1BE00E6C00C41517 /* LongPressRecordButtonSample */,
129 | );
130 | };
131 | /* End PBXProject section */
132 |
133 | /* Begin PBXResourcesBuildPhase section */
134 | BFD64F3D1BE00E6C00C41517 /* Resources */ = {
135 | isa = PBXResourcesBuildPhase;
136 | buildActionMask = 2147483647;
137 | files = (
138 | BFD64F4D1BE00E6C00C41517 /* LaunchScreen.storyboard in Resources */,
139 | BFD64F4A1BE00E6C00C41517 /* Assets.xcassets in Resources */,
140 | BFD64F481BE00E6C00C41517 /* Main.storyboard in Resources */,
141 | );
142 | runOnlyForDeploymentPostprocessing = 0;
143 | };
144 | /* End PBXResourcesBuildPhase section */
145 |
146 | /* Begin PBXSourcesBuildPhase section */
147 | BFD64F3B1BE00E6C00C41517 /* Sources */ = {
148 | isa = PBXSourcesBuildPhase;
149 | buildActionMask = 2147483647;
150 | files = (
151 | BFD64F451BE00E6C00C41517 /* ViewController.swift in Sources */,
152 | BF369CF51BE6BAD200E88DDC /* LongPressRecordButton.swift in Sources */,
153 | BFD64F431BE00E6C00C41517 /* AppDelegate.swift in Sources */,
154 | );
155 | runOnlyForDeploymentPostprocessing = 0;
156 | };
157 | /* End PBXSourcesBuildPhase section */
158 |
159 | /* Begin PBXVariantGroup section */
160 | BFD64F461BE00E6C00C41517 /* Main.storyboard */ = {
161 | isa = PBXVariantGroup;
162 | children = (
163 | BFD64F471BE00E6C00C41517 /* Base */,
164 | );
165 | name = Main.storyboard;
166 | sourceTree = "";
167 | };
168 | BFD64F4B1BE00E6C00C41517 /* LaunchScreen.storyboard */ = {
169 | isa = PBXVariantGroup;
170 | children = (
171 | BFD64F4C1BE00E6C00C41517 /* Base */,
172 | );
173 | name = LaunchScreen.storyboard;
174 | sourceTree = "";
175 | };
176 | /* End PBXVariantGroup section */
177 |
178 | /* Begin XCBuildConfiguration section */
179 | BFD64F4F1BE00E6C00C41517 /* Debug */ = {
180 | isa = XCBuildConfiguration;
181 | buildSettings = {
182 | ALWAYS_SEARCH_USER_PATHS = NO;
183 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
184 | CLANG_CXX_LIBRARY = "libc++";
185 | CLANG_ENABLE_MODULES = YES;
186 | CLANG_ENABLE_OBJC_ARC = YES;
187 | CLANG_WARN_BOOL_CONVERSION = YES;
188 | CLANG_WARN_CONSTANT_CONVERSION = YES;
189 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
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_OBJC_ROOT_CLASS = YES_ERROR;
195 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
196 | CLANG_WARN_UNREACHABLE_CODE = YES;
197 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
198 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
199 | COPY_PHASE_STRIP = NO;
200 | DEBUG_INFORMATION_FORMAT = dwarf;
201 | ENABLE_STRICT_OBJC_MSGSEND = YES;
202 | ENABLE_TESTABILITY = YES;
203 | GCC_C_LANGUAGE_STANDARD = gnu99;
204 | GCC_DYNAMIC_NO_PIC = NO;
205 | GCC_NO_COMMON_BLOCKS = YES;
206 | GCC_OPTIMIZATION_LEVEL = 0;
207 | GCC_PREPROCESSOR_DEFINITIONS = (
208 | "DEBUG=1",
209 | "$(inherited)",
210 | );
211 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
212 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
213 | GCC_WARN_UNDECLARED_SELECTOR = YES;
214 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
215 | GCC_WARN_UNUSED_FUNCTION = YES;
216 | GCC_WARN_UNUSED_VARIABLE = YES;
217 | IPHONEOS_DEPLOYMENT_TARGET = 9.1;
218 | MTL_ENABLE_DEBUG_INFO = YES;
219 | ONLY_ACTIVE_ARCH = YES;
220 | SDKROOT = iphoneos;
221 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
222 | };
223 | name = Debug;
224 | };
225 | BFD64F501BE00E6C00C41517 /* Release */ = {
226 | isa = XCBuildConfiguration;
227 | buildSettings = {
228 | ALWAYS_SEARCH_USER_PATHS = NO;
229 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
230 | CLANG_CXX_LIBRARY = "libc++";
231 | CLANG_ENABLE_MODULES = YES;
232 | CLANG_ENABLE_OBJC_ARC = YES;
233 | CLANG_WARN_BOOL_CONVERSION = YES;
234 | CLANG_WARN_CONSTANT_CONVERSION = YES;
235 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
236 | CLANG_WARN_EMPTY_BODY = YES;
237 | CLANG_WARN_ENUM_CONVERSION = YES;
238 | CLANG_WARN_INFINITE_RECURSION = YES;
239 | CLANG_WARN_INT_CONVERSION = YES;
240 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
241 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
242 | CLANG_WARN_UNREACHABLE_CODE = YES;
243 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
244 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
245 | COPY_PHASE_STRIP = NO;
246 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
247 | ENABLE_NS_ASSERTIONS = NO;
248 | ENABLE_STRICT_OBJC_MSGSEND = YES;
249 | GCC_C_LANGUAGE_STANDARD = gnu99;
250 | GCC_NO_COMMON_BLOCKS = YES;
251 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
252 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
253 | GCC_WARN_UNDECLARED_SELECTOR = YES;
254 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
255 | GCC_WARN_UNUSED_FUNCTION = YES;
256 | GCC_WARN_UNUSED_VARIABLE = YES;
257 | IPHONEOS_DEPLOYMENT_TARGET = 9.1;
258 | MTL_ENABLE_DEBUG_INFO = NO;
259 | SDKROOT = iphoneos;
260 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
261 | VALIDATE_PRODUCT = YES;
262 | };
263 | name = Release;
264 | };
265 | BFD64F521BE00E6C00C41517 /* Debug */ = {
266 | isa = XCBuildConfiguration;
267 | buildSettings = {
268 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
269 | INFOPLIST_FILE = "$(SRCROOT)/LongPressRecordButtonSample/Info.plist";
270 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
271 | PRODUCT_BUNDLE_IDENTIFIER = de.mathiaskoehnke.RecordButtonSample;
272 | PRODUCT_NAME = LongPressRecordButtonSample;
273 | SWIFT_VERSION = 3.0;
274 | };
275 | name = Debug;
276 | };
277 | BFD64F531BE00E6C00C41517 /* Release */ = {
278 | isa = XCBuildConfiguration;
279 | buildSettings = {
280 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
281 | INFOPLIST_FILE = "$(SRCROOT)/LongPressRecordButtonSample/Info.plist";
282 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
283 | PRODUCT_BUNDLE_IDENTIFIER = de.mathiaskoehnke.RecordButtonSample;
284 | PRODUCT_NAME = LongPressRecordButtonSample;
285 | SWIFT_VERSION = 3.0;
286 | };
287 | name = Release;
288 | };
289 | /* End XCBuildConfiguration section */
290 |
291 | /* Begin XCConfigurationList section */
292 | BFD64F3A1BE00E6B00C41517 /* Build configuration list for PBXProject "LongPressRecordButtonSample" */ = {
293 | isa = XCConfigurationList;
294 | buildConfigurations = (
295 | BFD64F4F1BE00E6C00C41517 /* Debug */,
296 | BFD64F501BE00E6C00C41517 /* Release */,
297 | );
298 | defaultConfigurationIsVisible = 0;
299 | defaultConfigurationName = Release;
300 | };
301 | BFD64F511BE00E6C00C41517 /* Build configuration list for PBXNativeTarget "LongPressRecordButtonSample" */ = {
302 | isa = XCConfigurationList;
303 | buildConfigurations = (
304 | BFD64F521BE00E6C00C41517 /* Debug */,
305 | BFD64F531BE00E6C00C41517 /* Release */,
306 | );
307 | defaultConfigurationIsVisible = 0;
308 | defaultConfigurationName = Release;
309 | };
310 | /* End XCConfigurationList section */
311 | };
312 | rootObject = BFD64F371BE00E6B00C41517 /* Project object */;
313 | }
314 |
--------------------------------------------------------------------------------
/LongPressRecordButton/LongPressRecordButton.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LongPressRecordButton.swift
3 | //
4 | // Copyright (c) 2015 Mathias Koehnke (http://www.mathiaskoehnke.com)
5 | //
6 | // Permission is hereby granted, free of charge, to any person obtaining a copy
7 | // of this software and associated documentation files (the "Software"), to deal
8 | // in the Software without restriction, including without limitation the rights
9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | // copies of the Software, and to permit persons to whom the Software is
11 | // furnished to do so, subject to the following conditions:
12 | //
13 | // The above copyright notice and this permission notice shall be included in
14 | // all copies or substantial portions of the Software.
15 | //
16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | // THE SOFTWARE.
23 |
24 |
25 | import UIKit
26 |
27 | //================================================
28 | // MARK: Delegate
29 | //================================================
30 |
31 | /// The delegate protocol of LongPressRecordButton.
32 | @objc public protocol LongPressRecordButtonDelegate {
33 | /// Tells the delegate that a long press has started.
34 | func longPressRecordButtonDidStartLongPress(_ button : LongPressRecordButton)
35 | /// Tells the delegate that a long press has finished.
36 | func longPressRecordButtonDidStopLongPress(_ button: LongPressRecordButton)
37 | /// Tells the delegate that a tool tip should be presented when a short press occured.
38 | @objc optional func longPressRecordButtonShouldShowToolTip(_ button : LongPressRecordButton) -> Bool
39 | /// Tells the delegate that a short press has occured and therefore a tooltip is shown.
40 | @objc optional func longPressRecordButtonDidShowToolTip(_ button : LongPressRecordButton)
41 | }
42 |
43 | //================================================
44 | // MARK: RecordButton
45 | //================================================
46 |
47 | /// The LongPressRecordButton class.
48 | @IBDesignable open class LongPressRecordButton : UIControl {
49 |
50 | /// The delegate of the LongPressRecordButton instance.
51 | open weak var delegate : LongPressRecordButtonDelegate?
52 |
53 | /// The minmal duration, that the record button is supposed
54 | /// to stay in the 'selected' state, once the long press has
55 | /// started.
56 | @IBInspectable open var minPressDuration : Float = 1.0
57 |
58 | /// The width of the outer ring of the record button.
59 | @IBInspectable open var ringWidth : CGFloat = 4.0 {
60 | didSet { redraw() }
61 | }
62 |
63 | /// The color of the outer ring of the record button.
64 | @IBInspectable open var ringColor : UIColor? = UIColor.white {
65 | didSet { redraw() }
66 | }
67 |
68 | /// The margin between the outer ring and inner circle
69 | /// of the record button.
70 | @IBInspectable open var circleMargin : CGFloat = 0.0 {
71 | didSet { redraw() }
72 | }
73 |
74 | /// The color of the inner circle of the record button.
75 | @IBInspectable open var circleColor : UIColor? = UIColor.red {
76 | didSet { redraw() }
77 | }
78 |
79 | /// The text that the tooltip is supposed to display,
80 | /// if the user did short-press the button.
81 | open lazy var toolTipText : String = {
82 | return "Tap and Hold"
83 | }()
84 |
85 | /// The font of the tooltip text.
86 | open var toolTipFont : UIFont = {
87 | return UIFont.systemFont(ofSize: 12.0)
88 | }()
89 |
90 | /// The background color of the tooltip.
91 | open var toolTipColor : UIColor = {
92 | return UIColor.white
93 | }()
94 |
95 | /// The text color of the tooltip.
96 | open var toolTipTextColor : UIColor = {
97 | return UIColor(white: 0.0, alpha: 0.8)
98 | }()
99 |
100 | /// Determines if the record button is enabled.
101 | override open var isEnabled: Bool {
102 | didSet {
103 | let state : UIControlState = isEnabled ? UIControlState() : .disabled
104 | circleLayer.fillColor = circleColorForState(state)?.cgColor
105 | ringLayer.strokeColor = ringColorForState(state)?.cgColor
106 | }
107 | }
108 |
109 | // MARK: Initializers
110 |
111 | /// Initializer
112 | override init (frame : CGRect) {
113 | super.init(frame : frame)
114 | commonInit()
115 | }
116 |
117 | /// Initializer
118 | convenience init () {
119 | self.init(frame:CGRect.zero)
120 | }
121 |
122 | /// Initializer
123 | required public init?(coder aDecoder: NSCoder) {
124 | super.init(coder: aDecoder)
125 | commonInit()
126 | }
127 |
128 |
129 | // MARK: Private
130 |
131 | fileprivate var longPressRecognizer : UILongPressGestureRecognizer!
132 | fileprivate var touchesStarted : CFTimeInterval?
133 | fileprivate var touchesEnded : Bool = false
134 | fileprivate var shouldShowTooltip : Bool = true
135 |
136 | fileprivate var ringLayer : CAShapeLayer!
137 | fileprivate var circleLayer : CAShapeLayer!
138 |
139 | fileprivate var outerRect : CGRect {
140 | return CGRect(x: ringWidth/2, y: ringWidth/2, width: bounds.size.width-ringWidth, height: bounds.size.height-ringWidth)
141 | }
142 |
143 | fileprivate var innerRect : CGRect {
144 | let innerX = outerRect.origin.x + (ringWidth/2) + circleMargin
145 | let innerY = outerRect.origin.y + (ringWidth/2) + circleMargin
146 | let innerWidth = outerRect.size.width - ringWidth - (circleMargin * 2)
147 | let innerHeight = outerRect.size.height - ringWidth - (circleMargin * 2)
148 | return CGRect(x: innerX, y: innerY, width: innerWidth, height: innerHeight)
149 | }
150 |
151 | fileprivate func commonInit() {
152 | backgroundColor = UIColor.clear
153 |
154 | ringLayer = CAShapeLayer()
155 | ringLayer.fillColor = UIColor.clear.cgColor
156 | ringLayer.frame = bounds
157 | layer.addSublayer(ringLayer)
158 |
159 | circleLayer = CAShapeLayer()
160 | circleLayer.frame = bounds
161 | layer.addSublayer(circleLayer)
162 |
163 | redraw()
164 |
165 | longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(LongPressRecordButton.handleLongPress(_:)))
166 | longPressRecognizer.cancelsTouchesInView = false
167 | longPressRecognizer.minimumPressDuration = 0.3
168 | self.addGestureRecognizer(longPressRecognizer)
169 | addTarget(self, action: #selector(LongPressRecordButton.handleShortPress(_:)), for: UIControlEvents.touchUpInside)
170 | }
171 |
172 | fileprivate func redraw() {
173 | ringLayer.lineWidth = ringWidth
174 | ringLayer.strokeColor = ringColor?.cgColor
175 | ringLayer.path = UIBezierPath(ovalIn: outerRect).cgPath
176 | ringLayer.setNeedsDisplay()
177 |
178 | circleLayer.fillColor = circleColor?.cgColor
179 | circleLayer.path = UIBezierPath(ovalIn: innerRect).cgPath
180 | circleLayer.setNeedsDisplay()
181 | }
182 |
183 | /// Sublayer layouting
184 | override open func layoutSubviews() {
185 | super.layoutSubviews()
186 | ringLayer.frame = bounds
187 | circleLayer.frame = bounds
188 | redraw()
189 | }
190 |
191 | @objc fileprivate func handleLongPress(_ recognizer: UILongPressGestureRecognizer) {
192 | if (recognizer.state == .began) {
193 | buttonPressed()
194 | } else if (recognizer.state == .ended) {
195 | buttonReleased()
196 | }
197 | }
198 |
199 | @objc fileprivate func handleShortPress(_ sender: AnyObject?) {
200 | if shouldShowTooltip {
201 | if isTooltipVisible() == false {
202 | if let delegate = delegate , delegate.longPressRecordButtonShouldShowToolTip?(self) == false {
203 | return
204 | }
205 | let tooltip = ToolTip(title: toolTipText, foregroundColor: toolTipTextColor, backgroundColor: toolTipColor, font: toolTipFont, recordButton: self)
206 | tooltip.show()
207 | delegate?.longPressRecordButtonDidShowToolTip?(self)
208 | }
209 | }
210 | shouldShowTooltip = true
211 | }
212 |
213 | fileprivate func isTooltipVisible() -> Bool {
214 | return layer.sublayers?.filter({ $0.isKind(of: ToolTip.self) }).first != nil
215 | }
216 |
217 | fileprivate func buttonPressed() {
218 | if touchesStarted == nil {
219 | circleLayer.fillColor = circleColor?.darkerColor().cgColor
220 | setNeedsDisplay()
221 | touchesStarted = CACurrentMediaTime()
222 | touchesEnded = false
223 | shouldShowTooltip = false
224 |
225 | let delayTime = DispatchTime.now() + Double(Int64(Double(minPressDuration) * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC)
226 | DispatchQueue.main.asyncAfter(deadline: delayTime) { [weak self] in
227 | if let strongSelf = self {
228 | if strongSelf.touchesEnded { strongSelf.buttonReleased() }
229 | }
230 | }
231 | delegate?.longPressRecordButtonDidStartLongPress(self)
232 | }
233 | }
234 |
235 | fileprivate func buttonReleased() {
236 | if let touchesStarted = touchesStarted , (CACurrentMediaTime() - touchesStarted) >= Double(minPressDuration) {
237 | self.touchesStarted = nil
238 | circleLayer.fillColor = circleColor?.cgColor
239 | delegate?.longPressRecordButtonDidStopLongPress(self)
240 | } else {
241 | touchesEnded = true
242 | }
243 | }
244 |
245 | fileprivate func ringColorForState(_ state : UIControlState) -> UIColor? {
246 | switch state {
247 | case UIControlState(): return ringColor
248 | case UIControlState.highlighted: return ringColor
249 | case UIControlState.disabled: return ringColor?.withAlphaComponent(0.5)
250 | case UIControlState.selected: return ringColor
251 | default: return nil
252 | }
253 | }
254 |
255 | fileprivate func circleColorForState(_ state: UIControlState) -> UIColor? {
256 | switch state {
257 | case UIControlState(): return circleColor
258 | case UIControlState.highlighted: return circleColor?.darkerColor()
259 | case UIControlState.disabled: return circleColor?.withAlphaComponent(0.5)
260 | case UIControlState.selected: return circleColor?.darkerColor()
261 | default: return nil
262 | }
263 | }
264 |
265 | /// @IBDesignable support
266 | open override func prepareForInterfaceBuilder() {
267 | super.prepareForInterfaceBuilder()
268 | backgroundColor = UIColor.clear
269 | }
270 | }
271 |
272 |
273 | //================================================
274 | // MARK: Extensions
275 | //================================================
276 |
277 | private extension NSAttributedString {
278 | func sizeToFit(_ maxSize: CGSize) -> CGSize {
279 | return boundingRect(with: maxSize, options:(NSStringDrawingOptions.usesLineFragmentOrigin), context:nil).size
280 | }
281 | }
282 |
283 | private extension Int {
284 | var radians : CGFloat {
285 | return CGFloat(self) * CGFloat(M_PI) / 180.0
286 | }
287 | }
288 |
289 | private extension UIColor {
290 | func darkerColor() -> UIColor {
291 | var r:CGFloat = 0, g:CGFloat = 0, b:CGFloat = 0, a:CGFloat = 0
292 | if self.getRed(&r, green: &g, blue: &b, alpha: &a){
293 | return UIColor(red: max(r - 0.2, 0.0), green: max(g - 0.2, 0.0), blue: max(b - 0.2, 0.0), alpha: a)
294 | }
295 | return UIColor()
296 | }
297 | }
298 |
299 |
300 | //================================================
301 | // MARK: ToolTip
302 | //================================================
303 |
304 | private class ToolTip : CAShapeLayer, CAAnimationDelegate {
305 |
306 | fileprivate weak var recordButton : LongPressRecordButton?
307 | fileprivate let defaultMargin : CGFloat = 5.0
308 | fileprivate let defaultArrowSize : CGFloat = 5.0
309 | fileprivate let defaultCornerRadius : CGFloat = 5.0
310 | fileprivate var textLayer : CATextLayer!
311 |
312 | init(title: String, foregroundColor: UIColor, backgroundColor: UIColor, font: UIFont, recordButton: LongPressRecordButton) {
313 | super.init()
314 | commonInit(title, foregroundColor: foregroundColor, backgroundColor: backgroundColor, font: font, recordButton: recordButton)
315 | }
316 |
317 | required init?(coder aDecoder: NSCoder) {
318 | super.init(coder: aDecoder)
319 | }
320 |
321 | fileprivate func commonInit(_ title: String, foregroundColor: UIColor, backgroundColor: UIColor, font: UIFont, recordButton: LongPressRecordButton) {
322 | self.recordButton = recordButton
323 |
324 | let rect = recordButton.bounds
325 | let text = NSAttributedString(string: title, attributes: [NSFontAttributeName : font, NSForegroundColorAttributeName : foregroundColor])
326 |
327 | // TextLayer
328 | textLayer = CATextLayer()
329 | textLayer.string = text
330 | textLayer.alignmentMode = kCAAlignmentCenter
331 | textLayer.contentsScale = UIScreen.main.scale
332 |
333 | // ShapeLayer
334 | let screenSize = UIScreen.main.bounds.size
335 | let basePoint = CGPoint(x: rect.origin.x + (rect.size.width / 2), y: rect.origin.y - (defaultMargin * 2))
336 | let baseSize = text.sizeToFit(screenSize)
337 |
338 | let x = basePoint.x - (baseSize.width / 2) - (defaultMargin * 2)
339 | let y = basePoint.y - baseSize.height - (defaultMargin * 2) - defaultArrowSize
340 | let width = baseSize.width + (defaultMargin * 4)
341 | let height = baseSize.height + (defaultMargin * 2) + defaultArrowSize
342 | frame = CGRect(x: x, y: y, width: width, height: height)
343 |
344 | path = toolTipPath(bounds, arrowSize: defaultArrowSize, radius: defaultCornerRadius).cgPath
345 | fillColor = backgroundColor.cgColor
346 | addSublayer(textLayer)
347 | }
348 |
349 | fileprivate func toolTipPath(_ frame: CGRect, arrowSize: CGFloat, radius: CGFloat) -> UIBezierPath {
350 | let mid = frame.midX
351 | let width = frame.maxX
352 | let height = frame.maxY
353 |
354 | let path = UIBezierPath()
355 | path.move(to: CGPoint(x: mid, y: height))
356 | path.addLine(to: CGPoint(x: mid - arrowSize, y: height - arrowSize))
357 | path.addLine(to: CGPoint(x: radius, y: height - arrowSize))
358 | path.addArc(withCenter: CGPoint(x: radius, y: height - arrowSize - radius), radius: radius, startAngle: 90.radians, endAngle: 180.radians, clockwise: true)
359 | path.addLine(to: CGPoint(x: 0, y: radius))
360 | path.addArc(withCenter: CGPoint(x: radius, y: radius), radius: radius, startAngle: 180.radians, endAngle: 270.radians, clockwise: true)
361 | path.addLine(to: CGPoint(x: width - radius, y: 0))
362 | path.addArc(withCenter: CGPoint(x: width - radius, y: radius), radius: radius, startAngle: 270.radians, endAngle: 0.radians, clockwise: true)
363 | path.addLine(to: CGPoint(x: width, y: height - arrowSize - radius))
364 | path.addArc(withCenter: CGPoint(x: width - radius, y: height - arrowSize - radius), radius: radius, startAngle: 0.radians, endAngle: 90.radians, clockwise: true)
365 | path.addLine(to: CGPoint(x: mid + arrowSize, y: height - arrowSize))
366 | path.addLine(to: CGPoint(x: mid, y: height))
367 | path.close()
368 | return path
369 | }
370 |
371 | override func layoutSublayers() {
372 | super.layoutSublayers()
373 | textLayer.frame = CGRect(x: defaultMargin, y: defaultMargin, width: bounds.size.width-(defaultMargin*2), height: bounds.size.height-(defaultMargin*2))
374 | }
375 |
376 | fileprivate func animation(_ fromTransform: CATransform3D, toTransform: CATransform3D) -> CASpringAnimation {
377 | let animation = CASpringAnimation(keyPath: "transform")
378 | animation.damping = 15
379 | animation.initialVelocity = 10
380 | animation.fillMode = kCAFillModeForwards
381 | animation.isRemovedOnCompletion = false
382 | animation.fromValue = NSValue(caTransform3D: fromTransform)
383 | animation.toValue = NSValue(caTransform3D: toTransform)
384 | animation.duration = animation.settlingDuration
385 | animation.delegate = self
386 | animation.autoreverses = true
387 | return animation
388 | }
389 |
390 | func show() {
391 | recordButton?.layer.addSublayer(self)
392 | let show = animation(CATransform3DMakeScale(0, 0, 1), toTransform: CATransform3DIdentity)
393 | add(show, forKey: "show")
394 | }
395 |
396 | func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
397 | removeFromSuperlayer()
398 | }
399 | }
400 |
--------------------------------------------------------------------------------