├── 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 | [![Twitter: @mkoehnke](https://img.shields.io/badge/contact-@mkoehnke-blue.svg?style=flat)](https://twitter.com/mkoehnke) 4 | [![Version](https://img.shields.io/cocoapods/v/LongPressRecordButton.svg?style=flat)](http://cocoadocs.org/docsets/LongPressRecordButton) 5 | [![License](https://img.shields.io/cocoapods/l/LongPressRecordButton.svg?style=flat)](http://cocoadocs.org/docsets/LongPressRecordButton) 6 | [![Platform](https://img.shields.io/cocoapods/p/LongPressRecordButton.svg?style=flat)](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 | --------------------------------------------------------------------------------