├── NKButton └── Classes │ ├── .gitkeep │ ├── NKButtonStack.swift │ └── NKButton.swift ├── _Pods.xcodeproj ├── demo.gif ├── screenshot.png ├── Example ├── NKButton │ ├── Images.xcassets │ │ ├── Contents.json │ │ ├── key.imageset │ │ │ ├── key.png │ │ │ ├── key@2x.png │ │ │ ├── key@3x.png │ │ │ └── Contents.json │ │ ├── facebook.imageset │ │ │ ├── facebook.png │ │ │ ├── facebook@2x.png │ │ │ ├── facebook@3x.png │ │ │ └── Contents.json │ │ ├── login.imageset │ │ │ ├── authorize_16x16@1x.png │ │ │ ├── authorize_16x16@2x.png │ │ │ ├── authorize_16x16@3x.png │ │ │ └── Contents.json │ │ ├── twitter.imageset │ │ │ ├── social_16x16@1x.png │ │ │ ├── social_16x16@2x.png │ │ │ ├── social_16x16@3x.png │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── AppDelegate.swift │ ├── Info.plist │ ├── Base.lproj │ │ ├── Main.storyboard │ │ └── LaunchScreen.xib │ └── ViewController.swift ├── Podfile ├── NKButton.xcodeproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ ├── xcshareddata │ │ └── xcschemes │ │ │ └── NKButton-Example.xcscheme │ └── project.pbxproj ├── NKButton.xcworkspace │ ├── xcshareddata │ │ ├── WorkspaceSettings.xcsettings │ │ └── IDEWorkspaceChecks.plist │ └── contents.xcworkspacedata ├── NKButton_Example.entitlements ├── Podfile.lock └── Tests │ ├── Info.plist │ └── Tests.swift ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── contents.xcworkspacedata ├── Package.resolved ├── Package.swift ├── LICENSE ├── NKButton.podspec ├── .gitignore └── README.md /NKButton/Classes/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /_Pods.xcodeproj: -------------------------------------------------------------------------------- 1 | Example/Pods/Pods.xcodeproj -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kennic/NKButton/HEAD/demo.gif -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kennic/NKButton/HEAD/screenshot.png -------------------------------------------------------------------------------- /Example/NKButton/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Example/NKButton/Images.xcassets/key.imageset/key.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kennic/NKButton/HEAD/Example/NKButton/Images.xcassets/key.imageset/key.png -------------------------------------------------------------------------------- /Example/NKButton/Images.xcassets/key.imageset/key@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kennic/NKButton/HEAD/Example/NKButton/Images.xcassets/key.imageset/key@2x.png -------------------------------------------------------------------------------- /Example/NKButton/Images.xcassets/key.imageset/key@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kennic/NKButton/HEAD/Example/NKButton/Images.xcassets/key.imageset/key@3x.png -------------------------------------------------------------------------------- /Example/NKButton/Images.xcassets/facebook.imageset/facebook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kennic/NKButton/HEAD/Example/NKButton/Images.xcassets/facebook.imageset/facebook.png -------------------------------------------------------------------------------- /Example/NKButton/Images.xcassets/facebook.imageset/facebook@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kennic/NKButton/HEAD/Example/NKButton/Images.xcassets/facebook.imageset/facebook@2x.png -------------------------------------------------------------------------------- /Example/NKButton/Images.xcassets/facebook.imageset/facebook@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kennic/NKButton/HEAD/Example/NKButton/Images.xcassets/facebook.imageset/facebook@3x.png -------------------------------------------------------------------------------- /Example/NKButton/Images.xcassets/login.imageset/authorize_16x16@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kennic/NKButton/HEAD/Example/NKButton/Images.xcassets/login.imageset/authorize_16x16@1x.png -------------------------------------------------------------------------------- /Example/NKButton/Images.xcassets/login.imageset/authorize_16x16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kennic/NKButton/HEAD/Example/NKButton/Images.xcassets/login.imageset/authorize_16x16@2x.png -------------------------------------------------------------------------------- /Example/NKButton/Images.xcassets/login.imageset/authorize_16x16@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kennic/NKButton/HEAD/Example/NKButton/Images.xcassets/login.imageset/authorize_16x16@3x.png -------------------------------------------------------------------------------- /Example/NKButton/Images.xcassets/twitter.imageset/social_16x16@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kennic/NKButton/HEAD/Example/NKButton/Images.xcassets/twitter.imageset/social_16x16@1x.png -------------------------------------------------------------------------------- /Example/NKButton/Images.xcassets/twitter.imageset/social_16x16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kennic/NKButton/HEAD/Example/NKButton/Images.xcassets/twitter.imageset/social_16x16@2x.png -------------------------------------------------------------------------------- /Example/NKButton/Images.xcassets/twitter.imageset/social_16x16@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kennic/NKButton/HEAD/Example/NKButton/Images.xcassets/twitter.imageset/social_16x16@3x.png -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/Podfile: -------------------------------------------------------------------------------- 1 | platform :ios, '9.0' 2 | use_frameworks! 3 | 4 | target 'NKButton_Example' do 5 | pod 'FrameLayoutKit' 6 | pod 'NKButton', :path => '../' 7 | # pod 'NVActivityIndicatorView/AppExtension' 8 | 9 | end 10 | -------------------------------------------------------------------------------- /Example/NKButton.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/NKButton.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Example/NKButton.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Example/NKButton.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/NKButton.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/NKButton_Example.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.network.client 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Example/NKButton/Images.xcassets/key.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "key.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "key@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "key@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /Example/NKButton/Images.xcassets/facebook.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "facebook.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "facebook@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "facebook@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /Example/NKButton/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // NKButton 4 | // 5 | // Created by Nam Kennic on 03/10/2018. 6 | // Copyright (c) 2018 Nam Kennic. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | return true 18 | } 19 | 20 | } 21 | 22 | -------------------------------------------------------------------------------- /Example/NKButton/Images.xcassets/twitter.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "social_16x16@1x.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "social_16x16@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "social_16x16@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /Example/NKButton/Images.xcassets/login.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "authorize_16x16@1x.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "authorize_16x16@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "authorize_16x16@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "FrameLayoutKit", 6 | "repositoryURL": "https://github.com/kennic/FrameLayoutKit.git", 7 | "state": { 8 | "branch": "master", 9 | "revision": "bd26f0a41efcd454d6d56899d3cb79d9c609d566", 10 | "version": null 11 | } 12 | }, 13 | { 14 | "package": "NVActivityIndicatorView", 15 | "repositoryURL": "https://github.com/ninjaprox/NVActivityIndicatorView.git", 16 | "state": { 17 | "branch": null, 18 | "revision": "4a4726d15367d82bd3e9e1f01625bd76dbbb0c98", 19 | "version": "4.8.0" 20 | } 21 | } 22 | ] 23 | }, 24 | "version": 1 25 | } 26 | -------------------------------------------------------------------------------- /Example/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - FrameLayoutKit (5.4) 3 | - NKButton (4.4): 4 | - FrameLayoutKit 5 | - NVActivityIndicatorView/AppExtension 6 | - NVActivityIndicatorView/AppExtension (4.8.0) 7 | 8 | DEPENDENCIES: 9 | - FrameLayoutKit 10 | - NKButton (from `../`) 11 | 12 | SPEC REPOS: 13 | trunk: 14 | - FrameLayoutKit 15 | - NVActivityIndicatorView 16 | 17 | EXTERNAL SOURCES: 18 | NKButton: 19 | :path: "../" 20 | 21 | SPEC CHECKSUMS: 22 | FrameLayoutKit: b68a8389317b45536f0ad41d67358de1e163c86d 23 | NKButton: 9afb9b8fba1f28061022b90a095395f010a05979 24 | NVActivityIndicatorView: d24b7ebcf80af5dcd994adb650e2b6c93379270f 25 | 26 | PODFILE CHECKSUM: 27dda54c13a35de23d659f7dafe6a47d20a069a3 27 | 28 | COCOAPODS: 1.11.0 29 | -------------------------------------------------------------------------------- /Example/Tests/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 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "NKButton", 8 | platforms: [.iOS(.v9)], 9 | products: [ 10 | .library( 11 | name: "NKButton", 12 | targets: ["NKButton"]), 13 | ], 14 | dependencies: [ 15 | .package(url: "https://github.com/kennic/FrameLayoutKit.git", .upToNextMajor(from: "7.0.5")), 16 | .package(url: "https://github.com/ninjaprox/NVActivityIndicatorView.git", .upToNextMajor(from: "4.8.0")), 17 | ], 18 | targets: [ 19 | .target( 20 | name: "NKButton", 21 | dependencies: ["FrameLayoutKit", "NVActivityIndicatorView"], 22 | path: "NKButton/Classes") 23 | ] 24 | ) 25 | -------------------------------------------------------------------------------- /Example/Tests/Tests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import NKButton 3 | 4 | class Tests: XCTestCase { 5 | 6 | override func setUp() { 7 | super.setUp() 8 | // Put setup code here. This method is called before the invocation of each test method in the class. 9 | } 10 | 11 | override func tearDown() { 12 | // Put teardown code here. This method is called after the invocation of each test method in the class. 13 | super.tearDown() 14 | } 15 | 16 | func testExample() { 17 | // This is an example of a functional test case. 18 | XCTAssert(true, "Pass") 19 | } 20 | 21 | func testPerformanceExample() { 22 | // This is an example of a performance test case. 23 | self.measure() { 24 | // Put the code you want to measure the time of here. 25 | } 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 Nam Kennic 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Example/NKButton/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ios-marketing", 45 | "size" : "1024x1024", 46 | "scale" : "1x" 47 | } 48 | ], 49 | "info" : { 50 | "version" : 1, 51 | "author" : "xcode" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Example/NKButton/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 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /NKButton.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'NKButton' 3 | s.version = '4.7.1' 4 | s.summary = 'A fully customizable UIButton' 5 | s.description = <<-DESC 6 | A fully customizable button that fills all lacked functions from UIButton like: 7 | + setBackgroundColor:forState: 8 | + setBorderColor:forState 9 | + setShadowColor:forState 10 | + setGradientColor:forState 11 | + cornerRadius and isRoundedButton 12 | + imageAlignment (top, left, bottom, right, topEdge, leftEdge, bottomEdge, rightEdge) 13 | + set spacing between image and text 14 | + set loading state with loading animation from NVActivityIndicator 15 | + a backgroundView to attach an UIVisualEffectView if you want 16 | + flash effect 17 | + hover gesture 18 | DESC 19 | 20 | s.homepage = 'https://github.com/kennic/NKButton' 21 | s.license = { :type => 'MIT', :file => 'LICENSE' } 22 | s.author = { 'Nam Kennic' => 'namkennic@me.com' } 23 | s.source = { :git => 'https://github.com/kennic/NKButton.git', :tag => s.version.to_s } 24 | s.social_media_url = 'https://twitter.com/namkennic' 25 | s.platform = :ios, '9.0' 26 | s.ios.deployment_target = '9.0' 27 | s.swift_version = '5.2' 28 | 29 | s.source_files = 'NKButton/Classes/*.swift' 30 | s.frameworks = 'UIKit' 31 | s.dependency 'FrameLayoutKit' 32 | s.dependency 'NVActivityIndicatorView/AppExtension' 33 | 34 | end 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | # Package.pins 40 | .build/ 41 | 42 | # CocoaPods 43 | # 44 | # We recommend against adding the Pods directory to your .gitignore. However 45 | # you should judge for yourself, the pros and cons are mentioned at: 46 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 47 | # 48 | # Pods/ 49 | 50 | # Carthage 51 | # 52 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 53 | # Carthage/Checkouts 54 | 55 | Carthage/Build 56 | 57 | # fastlane 58 | # 59 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 60 | # screenshots whenever they are needed. 61 | # For more information about the recommended setup visit: 62 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 63 | 64 | fastlane/report.xml 65 | fastlane/Preview.html 66 | fastlane/screenshots 67 | fastlane/test_output 68 | -------------------------------------------------------------------------------- /Example/NKButton/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 | -------------------------------------------------------------------------------- /Example/NKButton/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 25 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NKButton 2 | 3 | [![Version](https://img.shields.io/cocoapods/v/NKButton.svg?style=flat)](http://cocoapods.org/pods/NKButton) 4 | [![License](https://img.shields.io/cocoapods/l/NKButton.svg?style=flat)](http://cocoapods.org/pods/NKButton) 5 | [![Platform](https://img.shields.io/cocoapods/p/NKButton.svg?style=flat)](http://cocoapods.org/pods/NKButton) 6 | ![Swift](https://img.shields.io/badge/%20in-swift%204.2-orange.svg) 7 | 8 | A fully customizable UIButton 9 | 10 | ## Example 11 | 12 | To run the example project, clone the repo, and run `pod install` from the Example directory first. 13 | 14 | ![NKButton](https://github.com/kennic/NKButton/blob/master/demo.gif) 15 | 16 | ## Installation 17 | 18 | NKButton is available through `Swift Package Manager` (Recommended) and [CocoaPods](http://cocoapods.org): 19 | 20 | 21 | ```ruby 22 | pod 'NKButton' 23 | ``` 24 | 25 | ## Usage 26 | 27 | Creation and basic customization: 28 | ```swift 29 | let button = NKButton() 30 | button.title = "Button" 31 | button.setTitleColor(.black, for: .normal) // set title color for normal state 32 | button.setTitleColor(.white, for: .highlighted) // set title color for highlight state 33 | button.setTitleFont(normalFont, for: .normal) 34 | button.setTitleFont(boldFont, for: .highlight) 35 | button.setBackgroundColor(.blue, for: .normal) // set background color for normal state 36 | button.setBackgroundColor(.green, for: .highlighted) // set background color for highlight state 37 | button.spacing = 10.0 // space between icon and title 38 | button.imageAlignment = .top // icon alignment 39 | button.underlineTitleDisabled = true // no underline text when `Settings > Accessibility > Button Shapes` is ON 40 | button.isRoundedButton = true 41 | button.cornerRadius = 10.0 42 | button.extendSize = CGSize(width: 50, height: 20) // size that will be included in sizeThatFits 43 | ``` 44 | 45 | Add border: 46 | ```swift 47 | button.setBorderColor(.black, for: .normal) // set border color for normal state 48 | button.setBorderColor(.white, for: .highlighted) // set border color for highlight state 49 | button.setBorderSize(1.0, for: .normal) // border stroke size 50 | button.setBorderSize(2.0, for: .highlighted) 51 | ``` 52 | 53 | Add shadow: 54 | ```swift 55 | button.setShadowColor(.blue, for: .normal) // set shadow color for normal state 56 | button.setShadowColor(.green, for: .highlighted) // set shadow color for highlight state 57 | button.shadowOffset = CGSize(width: 0, height: 5) 58 | button.shadowOpacity = 0.6 59 | button.shadowRadius = 10 60 | ``` 61 | 62 | Add gradient color: 63 | ```swift 64 | button.setGradientColor([UIColor(white: 1.0, alpha: 0.5), UIColor(white: 1.0, alpha: 0.0)], for: .normal) // set gradient color for normal state 65 | button.setGradientColor([UIColor(white: 1.0, alpha: 0.0), UIColor(white: 1.0, alpha: 0.5)], for: .highlighted) // set gradient color for highlight state 66 | ``` 67 | 68 | Set loading state: 69 | 70 | ```swift 71 | button.loadingIndicatorStyle = .ballBeat // loading indicator style 72 | button.loadingIndicatorAlignment = .atImage // loading indicator alignment 73 | button.hideImageWhileLoading = true 74 | button.hideTitleWhileLoading = false 75 | 76 | button.isLoading = true // show loading indicator in the button, and button will be disabled automatically until setting isLoading = false 77 | ``` 78 | 79 | Flashing: 80 | ```swift 81 | button.startFlashing() 82 | button.startFlashing(flashDuration: 0.25, intensity: 0.9, repeatCount: 10) 83 | ``` 84 | 85 | ## Subscript syntax: 86 | ```swift 87 | button.titleColors[.normal] = .black 88 | button.titleFonts[.normal] = normalFont 89 | button.titleFonts[.highlight] = boldFont 90 | button.backgroundColors[.normal] = .white 91 | button.backgroundColors[.highlight] = .yellow 92 | button.borderColors[.normal] = .gray 93 | button.borderColors[[.highlight, .selected]] = .black 94 | button.shadowColors[.normal] = .black 95 | ``` 96 | 97 | ## Dependency 98 | 99 | NKButton uses [NVActivityIndicatorView](https://github.com/ninjaprox/NVActivityIndicatorView) for loading indicator, so it currently has 32 animation types. 100 | 101 | NKButton uses [FrameLayoutKit](https://github.com/kennic/FrameLayoutKit) for content layout so you can customize the layout easily 102 | 103 | ## Author 104 | 105 | Nam Kennic, namkennic@me.com 106 | 107 | ## License 108 | 109 | NKButton is available under the MIT license. See the LICENSE file for more info. 110 | -------------------------------------------------------------------------------- /Example/NKButton.xcodeproj/xcshareddata/xcschemes/NKButton-Example.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 47 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 65 | 66 | 67 | 68 | 78 | 80 | 86 | 87 | 88 | 89 | 90 | 91 | 97 | 99 | 105 | 106 | 107 | 108 | 110 | 111 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /Example/NKButton/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // NKButton 4 | // 5 | // Created by Nam Kennic on 03/10/2018. 6 | // Copyright (c) 2018 Nam Kennic. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import NKButton 11 | import FrameLayoutKit 12 | #if canImport(NVActivityIndicatorView) 13 | import NVActivityIndicatorView 14 | #endif 15 | 16 | extension NKButton { 17 | 18 | class func DefaultButton(title: String, color: UIColor) -> NKButton { 19 | let button = NKButton(title: title, buttonColor: color, shadowColor: color) 20 | button.title = title 21 | button.titleLabel?.font = UIFont(name: "Helvetica", size: 14) 22 | 23 | button.setBackgroundColor(color, for: .normal) 24 | button.setShadowColor(color, for: .normal) 25 | 26 | button.shadowOffset = CGSize(width: 0, height: 5) 27 | button.shadowOpacity = 0.6 28 | button.shadowRadius = 10 29 | 30 | button.isRoundedButton = true 31 | 32 | return button 33 | } 34 | 35 | } 36 | 37 | class ViewController: UIViewController { 38 | let loginButton = NKButton.DefaultButton(title: "SIGN IN", color: UIColor(red:0.10, green:0.58, blue:0.15, alpha:1.00)) 39 | let facebookButton = NKButton.DefaultButton(title: "FACEBOOK", color: UIColor(red:0.25, green:0.39, blue:0.80, alpha:1.00)) 40 | let twitterButton = NKButton.DefaultButton(title: "TWITTER", color: UIColor(red:0.42, green:0.67, blue:0.91, alpha:1.00)) 41 | let forgotButton = NKButton(title: "Forgot Password?", buttonColor: .clear) 42 | let flashButton = NKButton.DefaultButton(title: "TAP TO FLASH", color: UIColor(red:0.61, green:0.11, blue:0.08, alpha:1.00)) 43 | var frameLayout: StackFrameLayout! 44 | 45 | override func viewDidLoad() { 46 | super.viewDidLoad() 47 | 48 | loginButton.setImage(#imageLiteral(resourceName: "login"), for: .normal) 49 | #if canImport(NVActivityIndicatorView) 50 | loginButton.loadingIndicatorStyle = .ballScaleRippleMultiple 51 | #endif 52 | loginButton.loadingIndicatorAlignment = .center 53 | loginButton.underlineTitleDisabled = true 54 | loginButton.spacing = 10.0 // space between icon and title 55 | loginButton.extendSize = CGSize(width: 0, height: 20) 56 | loginButton.contentEdgeInsets = UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 10) 57 | loginButton.imageAlignment = .rightEdge(spacing: 10) 58 | loginButton.textAlignment = (.center, .right) 59 | loginButton.isRoundedButton = false 60 | loginButton.transitionToCircleWhenLoading = true 61 | 62 | let facebookIcon = #imageLiteral(resourceName: "facebook") 63 | facebookButton.setImage(facebookIcon, for: .normal) 64 | facebookButton.setImage(facebookIcon, for: .highlighted) 65 | facebookButton.setBackgroundColor(UIColor(red:0.45, green:0.59, blue:1.0, alpha:1.00), for: .highlighted) 66 | facebookButton.spacing = 10.0 // space between icon and title 67 | facebookButton.loadingIndicatorAlignment = .atImage 68 | facebookButton.underlineTitleDisabled = true 69 | #if canImport(NVActivityIndicatorView) 70 | facebookButton.loadingIndicatorStyle = .ballClipRotatePulse 71 | #endif 72 | facebookButton.extendSize = CGSize(width: 0, height: 20) 73 | facebookButton.contentEdgeInsets = UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 10) 74 | facebookButton.imageAlignment = .leftEdge(spacing: 0) 75 | facebookButton.isRoundedButton = false 76 | 77 | let twitterIcon = #imageLiteral(resourceName: "twitter") 78 | twitterButton.setImage(twitterIcon, for: .normal) 79 | twitterButton.setImage(twitterIcon, for: .highlighted) 80 | twitterButton.setBackgroundColor(UIColor(red:0.45, green:0.59, blue:1.0, alpha:1.00), for: .highlighted) 81 | twitterButton.setGradientColor([UIColor(white: 1.0, alpha: 0.5), UIColor(white: 1.0, alpha: 0.0)], for: .normal) 82 | twitterButton.setGradientColor([UIColor(white: 1.0, alpha: 0.0), UIColor(white: 1.0, alpha: 0.5)], for: .highlighted) 83 | twitterButton.spacing = 10.0 // space between icon and title 84 | twitterButton.imageAlignment = .top 85 | twitterButton.titleLabel?.textAlignment = .center 86 | twitterButton.loadingIndicatorAlignment = .atImage 87 | twitterButton.hideImageWhileLoading = true 88 | twitterButton.hideTitleWhileLoading = false 89 | twitterButton.underlineTitleDisabled = true 90 | #if canImport(NVActivityIndicatorView) 91 | twitterButton.loadingIndicatorStyle = .ballBeat 92 | #endif 93 | twitterButton.isRoundedButton = false 94 | twitterButton.cornerRadius = 10.0 95 | twitterButton.extendSize = CGSize(width: 50, height: 20) 96 | 97 | forgotButton.setImage(#imageLiteral(resourceName: "key"), for: .normal) 98 | forgotButton.setTitleColor(.gray, for: .normal) 99 | forgotButton.setTitleColor(.gray, for: .highlighted) 100 | forgotButton.setTitleColor(.gray, for: .disabled) 101 | forgotButton.showsTouchWhenHighlighted = true 102 | forgotButton.titleLabel?.font = UIFont(name: "Helvetica", size: 14) 103 | forgotButton.spacing = 5.0 // space between icon and title 104 | forgotButton.autoSetDisableColor = false 105 | forgotButton.isRoundedButton = true 106 | forgotButton.extendSize = CGSize(width: 20, height: 20) 107 | #if !canImport(NVActivityIndicatorView) 108 | forgotButton.loadingIndicatorStyle = .gray 109 | #endif 110 | forgotButton.borderSizes[.normal] = 1 111 | forgotButton.borderColors[.normal] = .gray 112 | forgotButton.borderDashPatterns[.normal] = [2, 2] 113 | 114 | flashButton.flashColor = .red 115 | flashButton.underlineTitleDisabled = true 116 | flashButton.extendSize = CGSize(width: 0, height: 20) 117 | 118 | let allButtons = [loginButton, facebookButton, twitterButton, forgotButton, flashButton] 119 | allButtons.forEach { (button) in 120 | button.addTarget(self, action: #selector(onButtonSelected), for: .touchUpInside) 121 | button.backgroundColors[.hovered] = .red 122 | if #available(iOS 13.4, *) { 123 | button.enablePointerInteraction() 124 | } 125 | } 126 | 127 | frameLayout = StackFrameLayout(axis: .vertical, distribution: .top, views: allButtons) 128 | frameLayout.isIntrinsicSizeEnabled = true 129 | frameLayout.spacing = 40 130 | // frameLayout.debug = true // uncomment this to see how frameLayout layout its contents 131 | 132 | view.addSubview(loginButton) 133 | view.addSubview(facebookButton) 134 | view.addSubview(twitterButton) 135 | view.addSubview(forgotButton) 136 | view.addSubview(flashButton) 137 | view.addSubview(frameLayout) 138 | 139 | // Example of NKButtonStack usage: 140 | 141 | let buttonStack = NKButtonStack() 142 | buttonStack.backgroundColor = .systemRed 143 | buttonStack.borderSize = 2 144 | buttonStack.borderColor = .systemRed 145 | buttonStack.shadowColor = .gray 146 | buttonStack.shadowRadius = 4 147 | buttonStack.shadowOpacity = 1.0 148 | 149 | buttonStack.configuration { button, item, index in 150 | button.backgroundColors[.normal] = .lightGray 151 | button.backgroundColors[.highlighted] = .gray 152 | button.backgroundColors[.selected] = .red 153 | button.backgroundColors[[.selected, .highlighted]] = .green 154 | button.title = item.title 155 | button.setTitleFont(.systemFont(ofSize: 14, weight: .regular), for: .normal) 156 | button.setTitleFont(.systemFont(ofSize: 15, weight: .bold), for: .selected) 157 | button.extendSize = CGSize(width: 20, height: 20) 158 | } 159 | .selection { button, item, index in 160 | print("Selected: \(index) - \(item)") 161 | } 162 | 163 | buttonStack.items = [NKButtonItem(title: "Section A"), 164 | NKButtonItem(title: "Section B"), 165 | NKButtonItem(title: "Section C")] 166 | view.addSubview(buttonStack) 167 | buttonStack.isRounded = true 168 | frameLayout + buttonStack 169 | } 170 | 171 | override func viewDidLayoutSubviews() { 172 | super.viewDidLayoutSubviews() 173 | 174 | let viewSize = view.bounds.size 175 | let contentSize = frameLayout.sizeThatFits(CGSize(width: viewSize.width * 0.9, height: viewSize.height)) 176 | frameLayout.frame = CGRect(x: (viewSize.width - contentSize.width)/2, y: (viewSize.height - contentSize.height)/2, width: contentSize.width, height: contentSize.height) 177 | } 178 | 179 | @objc func onButtonSelected(_ button: NKButton) { 180 | print("Button Selected") 181 | 182 | if button == flashButton { 183 | button.startFlashing(flashDuration: 0.25, intensity: 0.9, repeatCount: 10) 184 | return 185 | } 186 | 187 | button.isLoading = true 188 | DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { 189 | button.isLoading = false 190 | } 191 | } 192 | 193 | } 194 | 195 | -------------------------------------------------------------------------------- /NKButton/Classes/NKButtonStack.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NKButtonStack.swift 3 | // NKButton 4 | // 5 | // Created by Nam Kennic on 8/23/17. 6 | // Copyright © 2017 Nam Kennic. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import FrameLayoutKit 11 | 12 | public struct NKButtonItem { 13 | public var title: String? 14 | public var image: UIImage? 15 | public var selectedImage: UIImage? 16 | public var userInfo: Any? 17 | 18 | public init(title: String?, image: UIImage? = nil, selectedImage: UIImage? = nil, userInfo: Any? = nil) { 19 | self.title = title 20 | self.image = image 21 | self.selectedImage = selectedImage 22 | self.userInfo = userInfo 23 | } 24 | } 25 | 26 | public enum NKButtonStackSelectionMode { 27 | case momentary 28 | case singleSelection 29 | case multiSelection 30 | } 31 | 32 | public typealias NKButtonCreationBlock = (NKButtonItem, Int) -> T 33 | public typealias NKButtonSelectionBlock = (T, NKButtonItem, Int) -> Void 34 | 35 | open class NKButtonStack: UIControl { 36 | 37 | open var items: [NKButtonItem]? = nil { 38 | didSet { 39 | updateLayout() 40 | setNeedsLayout() 41 | } 42 | } 43 | 44 | public var buttons: [T] { frameLayout.frameLayouts.map( { return $0.targetView as! T }) } 45 | public var firstButton: T? { frameLayout.firstFrameLayout?.targetView as? T } 46 | public var lastButton: T? { frameLayout.lastFrameLayout?.targetView as? T } 47 | 48 | open var spacing: CGFloat { 49 | get { frameLayout.spacing } 50 | set { 51 | frameLayout.spacing = newValue 52 | setNeedsLayout() 53 | } 54 | } 55 | 56 | open var contentEdgeInsets: UIEdgeInsets { 57 | get { frameLayout.edgeInsets } 58 | set { 59 | frameLayout.edgeInsets = newValue 60 | setNeedsLayout() 61 | } 62 | } 63 | 64 | open var cornerRadius: CGFloat = 0 { 65 | didSet { 66 | guard cornerRadius != oldValue else { return } 67 | setNeedsDisplay() 68 | } 69 | } 70 | 71 | /** Shadow color */ 72 | open var shadowColor: UIColor? = nil { 73 | didSet { 74 | guard shadowColor != oldValue else { return } 75 | setNeedsDisplay() 76 | } 77 | } 78 | 79 | /** Shadow radius */ 80 | open var shadowRadius: CGFloat = 0 { 81 | didSet { 82 | guard shadowRadius != oldValue else { return } 83 | setNeedsDisplay() 84 | } 85 | } 86 | 87 | /** Shadow opacity */ 88 | open var shadowOpacity: Float = 0.5 { 89 | didSet { 90 | guard shadowOpacity != oldValue else { return } 91 | setNeedsDisplay() 92 | } 93 | } 94 | 95 | /** Shadow offset */ 96 | open var shadowOffset: CGSize = .zero { 97 | didSet { 98 | guard shadowOffset != oldValue else { return } 99 | setNeedsDisplay() 100 | } 101 | } 102 | 103 | /** Border color */ 104 | open var borderColor: UIColor? = nil { 105 | didSet { 106 | guard borderColor != oldValue else { return } 107 | setNeedsDisplay() 108 | } 109 | } 110 | 111 | /** Size of border */ 112 | open var borderSize: CGFloat = 0 { 113 | didSet { 114 | guard borderSize != oldValue else { return } 115 | setNeedsDisplay() 116 | } 117 | } 118 | 119 | /** Border dash pattern */ 120 | open var borderDashPattern: [NSNumber]? = nil { 121 | didSet { 122 | guard borderDashPattern != oldValue else { return } 123 | setNeedsDisplay() 124 | } 125 | } 126 | 127 | /** Border color */ 128 | private var _backgroundColor: UIColor? = nil 129 | open override var backgroundColor: UIColor?{ 130 | get { _backgroundColor } 131 | set { 132 | _backgroundColor = newValue 133 | setNeedsDisplay() 134 | super.backgroundColor = .clear 135 | } 136 | } 137 | 138 | open var isRounded: Bool = false { 139 | didSet { 140 | guard isRounded != oldValue else { return } 141 | setNeedsLayout() 142 | } 143 | } 144 | 145 | override open var frame: CGRect { 146 | didSet { setNeedsLayout() } 147 | } 148 | 149 | override open var bounds: CGRect { 150 | didSet { setNeedsLayout() } 151 | } 152 | 153 | public var selectedIndex: Int = -1 { 154 | didSet { 155 | buttons.forEach { $0.isSelected = selectedIndex == $0.tag } 156 | } 157 | } 158 | 159 | public var selectedIndexes: [Int] { 160 | get { buttons.filter { $0.isSelected }.map { $0.tag } } 161 | set { buttons.forEach { $0.isSelected = newValue.contains($0.tag) } } 162 | } 163 | 164 | public var axis: NKLayoutAxis { 165 | get { frameLayout.axis } 166 | set { 167 | frameLayout.axis = newValue 168 | setNeedsLayout() 169 | } 170 | } 171 | 172 | @available(*, deprecated, message: "Use `selectionMode` instead") 173 | public var isMomentary = false { 174 | didSet { 175 | selectionMode = isMomentary ? .momentary : .singleSelection 176 | } 177 | } 178 | 179 | public var selectionMode: NKButtonStackSelectionMode = .singleSelection 180 | public var creationBlock: NKButtonCreationBlock? = nil 181 | public var configurationBlock: NKButtonSelectionBlock? = nil 182 | public var selectionBlock: NKButtonSelectionBlock? = nil 183 | 184 | public let scrollView = UIScrollView() 185 | public let frameLayout = StackFrameLayout(axis: .horizontal, distribution: .equal) 186 | 187 | fileprivate let shadowLayer = CAShapeLayer() 188 | fileprivate let backgroundLayer = CAShapeLayer() 189 | 190 | // MARK: - 191 | 192 | convenience public init(items: [NKButtonItem], axis: NKLayoutAxis = .horizontal) { 193 | self.init() 194 | 195 | self.axis = axis 196 | defer { 197 | self.items = items 198 | } 199 | } 200 | 201 | public init() { 202 | super.init(frame: .zero) 203 | 204 | layer.addSublayer(shadowLayer) 205 | layer.addSublayer(backgroundLayer) 206 | 207 | frameLayout.spacing = 1.0 208 | frameLayout.isIntrinsicSizeEnabled = true 209 | frameLayout.shouldCacheSize = false 210 | 211 | scrollView.bounces = true 212 | scrollView.alwaysBounceHorizontal = false 213 | scrollView.alwaysBounceVertical = false 214 | scrollView.isDirectionalLockEnabled = true 215 | scrollView.showsVerticalScrollIndicator = false 216 | scrollView.showsHorizontalScrollIndicator = false 217 | scrollView.clipsToBounds = false 218 | scrollView.delaysContentTouches = false 219 | scrollView.addSubview(frameLayout) 220 | addSubview(scrollView) 221 | } 222 | 223 | required public init?(coder aDecoder: NSCoder) { 224 | super.init(coder: aDecoder) 225 | } 226 | 227 | override open func sizeThatFits(_ size: CGSize) -> CGSize { 228 | return frameLayout.sizeThatFits(size) 229 | } 230 | 231 | override open func draw(_ rect: CGRect) { 232 | super.draw(rect) 233 | 234 | let backgroundFrame = bounds 235 | let fillColor = backgroundColor 236 | let strokeColor = borderColor 237 | let strokeSize = borderSize 238 | let roundedPath = UIBezierPath(roundedRect: backgroundFrame, cornerRadius: cornerRadius) 239 | let path = roundedPath.cgPath 240 | 241 | backgroundLayer.path = path 242 | backgroundLayer.fillColor = fillColor?.cgColor 243 | backgroundLayer.strokeColor = strokeColor?.cgColor 244 | backgroundLayer.lineWidth = strokeSize 245 | backgroundLayer.miterLimit = roundedPath.miterLimit 246 | backgroundLayer.lineDashPattern = borderDashPattern 247 | 248 | if let shadowColor = shadowColor { 249 | shadowLayer.isHidden = false 250 | shadowLayer.path = path 251 | shadowLayer.shadowPath = path 252 | shadowLayer.fillColor = shadowColor.cgColor 253 | shadowLayer.shadowColor = shadowColor.cgColor 254 | shadowLayer.shadowRadius = shadowRadius 255 | shadowLayer.shadowOpacity = shadowOpacity 256 | shadowLayer.shadowOffset = shadowOffset 257 | } 258 | else { 259 | shadowLayer.isHidden = true 260 | } 261 | } 262 | 263 | override open func layoutSubviews() { 264 | super.layoutSubviews() 265 | 266 | shadowLayer.frame = bounds 267 | backgroundLayer.frame = bounds 268 | 269 | let viewSize = bounds.size 270 | let contentSize = frameLayout.sizeThatFits(CGSize(width: CGFloat.infinity, height: CGFloat.infinity)) 271 | scrollView.contentSize = contentSize 272 | scrollView.frame = bounds 273 | 274 | var contentFrame = bounds 275 | if frameLayout.axis == .horizontal, contentSize.width > viewSize.width { 276 | contentFrame.size.width = contentSize.width 277 | scrollView.delaysContentTouches = true 278 | } 279 | else if frameLayout.axis == .vertical, contentSize.height > viewSize.height { 280 | contentFrame.size.height = contentSize.height 281 | scrollView.delaysContentTouches = true 282 | } 283 | else { 284 | scrollView.delaysContentTouches = false 285 | } 286 | 287 | frameLayout.frame = contentFrame 288 | 289 | if isRounded { 290 | cornerRadius = viewSize.height / 2 291 | setNeedsDisplay() 292 | } 293 | 294 | if cornerRadius > 0 { 295 | scrollView.layer.cornerRadius = cornerRadius 296 | scrollView.layer.masksToBounds = true 297 | } 298 | else { 299 | scrollView.layer.cornerRadius = 0 300 | scrollView.layer.masksToBounds = false 301 | } 302 | } 303 | 304 | // MARK: - 305 | 306 | public func button(at index: Int) -> T? { 307 | return frameLayout.frameLayout(at: index)?.targetView as? T 308 | } 309 | 310 | open func setShadow(color: UIColor?, radius: CGFloat, opacity: Float = 1.0, offset: CGSize = .zero) { 311 | self.shadowColor = color 312 | self.shadowOpacity = opacity 313 | self.shadowRadius = radius 314 | self.shadowOffset = offset 315 | } 316 | 317 | @discardableResult 318 | public func creation(_ block: @escaping NKButtonCreationBlock) -> Self { 319 | creationBlock = block 320 | return self 321 | } 322 | 323 | @discardableResult 324 | public func configuration(_ block: @escaping NKButtonSelectionBlock) -> Self { 325 | configurationBlock = block 326 | return self 327 | } 328 | 329 | @discardableResult 330 | public func selection(_ block: @escaping NKButtonSelectionBlock) -> Self { 331 | selectionBlock = block 332 | return self 333 | } 334 | 335 | // MARK: - 336 | 337 | fileprivate func updateLayout() { 338 | guard let buttonItems = items else { 339 | frameLayout.enumerate({ (layout, index, stop) in 340 | if let button = layout.targetView as? T { 341 | button.removeTarget(self, action: #selector(onButtonSelected(_:)), for: .touchUpInside) 342 | button.removeFromSuperview() 343 | } 344 | }) 345 | 346 | frameLayout.removeAll(autoRemoveTargetView: true) 347 | return 348 | } 349 | 350 | let total = buttonItems.count 351 | 352 | if frameLayout.frameLayouts.count > total { 353 | frameLayout.enumerate({ (layout, index, stop) in 354 | if Int(index) >= Int(total) { 355 | if let button = layout.targetView as? T { 356 | button.removeTarget(self, action: #selector(onButtonSelected(_:)), for: .touchUpInside) 357 | button.removeFromSuperview() 358 | } 359 | } 360 | }) 361 | } 362 | 363 | frameLayout.numberOfFrameLayouts = total 364 | 365 | frameLayout.enumerate({ (layout, idx, stop) in 366 | let index = Int(idx) 367 | let buttonItem = items![index] 368 | let button = layout.targetView as? T ?? creationBlock?(buttonItem, index) ?? T() 369 | button.tag = index 370 | button.addTarget(self, action: #selector(onButtonSelected(_:)), for: .touchUpInside) 371 | scrollView.addSubview(button) 372 | layout.targetView = button 373 | 374 | guard let configurationBlock = configurationBlock else { 375 | button.setTitle(buttonItem.title, for: .normal) 376 | button.setImage(buttonItem.image, for: .normal) 377 | 378 | if buttonItem.selectedImage != nil { 379 | button.setImage(buttonItem.selectedImage, for: .highlighted) 380 | button.setImage(buttonItem.selectedImage, for: .selected) 381 | } 382 | return 383 | } 384 | 385 | configurationBlock(button , buttonItem, index) 386 | }) 387 | } 388 | 389 | @objc fileprivate func onButtonSelected(_ sender: UIButton) { 390 | let index = sender.tag 391 | 392 | if selectionMode == .singleSelection { 393 | selectedIndex = index 394 | } 395 | else if selectionMode == .multiSelection { 396 | sender.isSelected = !sender.isSelected 397 | } 398 | 399 | if let item = items?[index], let button = sender as? T { 400 | selectionBlock?(button, item, index) 401 | } 402 | 403 | sendActions(for: .valueChanged) 404 | } 405 | 406 | } 407 | -------------------------------------------------------------------------------- /Example/NKButton.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 607FACD61AFB9204008FA782 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACD51AFB9204008FA782 /* AppDelegate.swift */; }; 11 | 607FACD81AFB9204008FA782 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACD71AFB9204008FA782 /* ViewController.swift */; }; 12 | 607FACDB1AFB9204008FA782 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 607FACD91AFB9204008FA782 /* Main.storyboard */; }; 13 | 607FACDD1AFB9204008FA782 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 607FACDC1AFB9204008FA782 /* Images.xcassets */; }; 14 | 607FACE01AFB9204008FA782 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 607FACDE1AFB9204008FA782 /* LaunchScreen.xib */; }; 15 | EC9F584A926ECB0B0EB95BCF /* Pods_NKButton_Example.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E84B970779C72BF7C9530B4D /* Pods_NKButton_Example.framework */; }; 16 | /* End PBXBuildFile section */ 17 | 18 | /* Begin PBXFileReference section */ 19 | 1E467AA746CDF43320136440 /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = ""; }; 20 | 5D56EA8FF45BDA9A9350C52E /* LICENSE */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = LICENSE; path = ../LICENSE; sourceTree = ""; }; 21 | 607FACD01AFB9204008FA782 /* NKButton_Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = NKButton_Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; 22 | 607FACD41AFB9204008FA782 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 23 | 607FACD51AFB9204008FA782 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 24 | 607FACD71AFB9204008FA782 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 25 | 607FACDA1AFB9204008FA782 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 26 | 607FACDC1AFB9204008FA782 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 27 | 607FACDF1AFB9204008FA782 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; }; 28 | 607FACEA1AFB9204008FA782 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 29 | 607FACEB1AFB9204008FA782 /* Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tests.swift; sourceTree = ""; }; 30 | 6311EB8A244E9D5100EAFED7 /* NKButton_Example.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NKButton_Example.entitlements; sourceTree = ""; }; 31 | E81A17708EA8EC32B91F40DC /* Pods-NKButton_Example.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NKButton_Example.debug.xcconfig"; path = "Target Support Files/Pods-NKButton_Example/Pods-NKButton_Example.debug.xcconfig"; sourceTree = ""; }; 32 | E84B970779C72BF7C9530B4D /* Pods_NKButton_Example.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_NKButton_Example.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 33 | ECF7CAE7E2524B386CD28B7E /* NKButton.podspec */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = NKButton.podspec; path = ../NKButton.podspec; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; 34 | F8D16959655523722DDFA0A2 /* Pods-NKButton_Example.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NKButton_Example.release.xcconfig"; path = "Target Support Files/Pods-NKButton_Example/Pods-NKButton_Example.release.xcconfig"; sourceTree = ""; }; 35 | /* End PBXFileReference section */ 36 | 37 | /* Begin PBXFrameworksBuildPhase section */ 38 | 607FACCD1AFB9204008FA782 /* Frameworks */ = { 39 | isa = PBXFrameworksBuildPhase; 40 | buildActionMask = 2147483647; 41 | files = ( 42 | EC9F584A926ECB0B0EB95BCF /* Pods_NKButton_Example.framework in Frameworks */, 43 | ); 44 | runOnlyForDeploymentPostprocessing = 0; 45 | }; 46 | /* End PBXFrameworksBuildPhase section */ 47 | 48 | /* Begin PBXGroup section */ 49 | 607FACC71AFB9204008FA782 = { 50 | isa = PBXGroup; 51 | children = ( 52 | 6311EB8A244E9D5100EAFED7 /* NKButton_Example.entitlements */, 53 | 607FACF51AFB993E008FA782 /* Podspec Metadata */, 54 | 607FACD21AFB9204008FA782 /* Example for NKButton */, 55 | 607FACE81AFB9204008FA782 /* Tests */, 56 | 607FACD11AFB9204008FA782 /* Products */, 57 | ED3CAF73C1FAA46AB2F0CFA8 /* Pods */, 58 | EE6F157CAA80E1EF4748DB0F /* Frameworks */, 59 | ); 60 | sourceTree = ""; 61 | }; 62 | 607FACD11AFB9204008FA782 /* Products */ = { 63 | isa = PBXGroup; 64 | children = ( 65 | 607FACD01AFB9204008FA782 /* NKButton_Example.app */, 66 | ); 67 | name = Products; 68 | sourceTree = ""; 69 | }; 70 | 607FACD21AFB9204008FA782 /* Example for NKButton */ = { 71 | isa = PBXGroup; 72 | children = ( 73 | 607FACD51AFB9204008FA782 /* AppDelegate.swift */, 74 | 607FACD71AFB9204008FA782 /* ViewController.swift */, 75 | 607FACD91AFB9204008FA782 /* Main.storyboard */, 76 | 607FACDC1AFB9204008FA782 /* Images.xcassets */, 77 | 607FACDE1AFB9204008FA782 /* LaunchScreen.xib */, 78 | 607FACD31AFB9204008FA782 /* Supporting Files */, 79 | ); 80 | name = "Example for NKButton"; 81 | path = NKButton; 82 | sourceTree = ""; 83 | }; 84 | 607FACD31AFB9204008FA782 /* Supporting Files */ = { 85 | isa = PBXGroup; 86 | children = ( 87 | 607FACD41AFB9204008FA782 /* Info.plist */, 88 | ); 89 | name = "Supporting Files"; 90 | sourceTree = ""; 91 | }; 92 | 607FACE81AFB9204008FA782 /* Tests */ = { 93 | isa = PBXGroup; 94 | children = ( 95 | 607FACEB1AFB9204008FA782 /* Tests.swift */, 96 | 607FACE91AFB9204008FA782 /* Supporting Files */, 97 | ); 98 | path = Tests; 99 | sourceTree = ""; 100 | }; 101 | 607FACE91AFB9204008FA782 /* Supporting Files */ = { 102 | isa = PBXGroup; 103 | children = ( 104 | 607FACEA1AFB9204008FA782 /* Info.plist */, 105 | ); 106 | name = "Supporting Files"; 107 | sourceTree = ""; 108 | }; 109 | 607FACF51AFB993E008FA782 /* Podspec Metadata */ = { 110 | isa = PBXGroup; 111 | children = ( 112 | ECF7CAE7E2524B386CD28B7E /* NKButton.podspec */, 113 | 1E467AA746CDF43320136440 /* README.md */, 114 | 5D56EA8FF45BDA9A9350C52E /* LICENSE */, 115 | ); 116 | name = "Podspec Metadata"; 117 | sourceTree = ""; 118 | }; 119 | ED3CAF73C1FAA46AB2F0CFA8 /* Pods */ = { 120 | isa = PBXGroup; 121 | children = ( 122 | E81A17708EA8EC32B91F40DC /* Pods-NKButton_Example.debug.xcconfig */, 123 | F8D16959655523722DDFA0A2 /* Pods-NKButton_Example.release.xcconfig */, 124 | ); 125 | path = Pods; 126 | sourceTree = ""; 127 | }; 128 | EE6F157CAA80E1EF4748DB0F /* Frameworks */ = { 129 | isa = PBXGroup; 130 | children = ( 131 | E84B970779C72BF7C9530B4D /* Pods_NKButton_Example.framework */, 132 | ); 133 | name = Frameworks; 134 | sourceTree = ""; 135 | }; 136 | /* End PBXGroup section */ 137 | 138 | /* Begin PBXNativeTarget section */ 139 | 607FACCF1AFB9204008FA782 /* NKButton_Example */ = { 140 | isa = PBXNativeTarget; 141 | buildConfigurationList = 607FACEF1AFB9204008FA782 /* Build configuration list for PBXNativeTarget "NKButton_Example" */; 142 | buildPhases = ( 143 | 1B8A756768417BF0B6C49B76 /* [CP] Check Pods Manifest.lock */, 144 | 607FACCC1AFB9204008FA782 /* Sources */, 145 | 607FACCD1AFB9204008FA782 /* Frameworks */, 146 | 607FACCE1AFB9204008FA782 /* Resources */, 147 | 71F8A181F57D0E3F4AC62EED /* [CP] Embed Pods Frameworks */, 148 | ); 149 | buildRules = ( 150 | ); 151 | dependencies = ( 152 | ); 153 | name = NKButton_Example; 154 | productName = NKButton; 155 | productReference = 607FACD01AFB9204008FA782 /* NKButton_Example.app */; 156 | productType = "com.apple.product-type.application"; 157 | }; 158 | /* End PBXNativeTarget section */ 159 | 160 | /* Begin PBXProject section */ 161 | 607FACC81AFB9204008FA782 /* Project object */ = { 162 | isa = PBXProject; 163 | attributes = { 164 | LastSwiftUpdateCheck = 0830; 165 | LastUpgradeCheck = 0940; 166 | ORGANIZATIONNAME = CocoaPods; 167 | TargetAttributes = { 168 | 607FACCF1AFB9204008FA782 = { 169 | CreatedOnToolsVersion = 6.3.1; 170 | DevelopmentTeam = 385YL4KG69; 171 | LastSwiftMigration = 0900; 172 | }; 173 | }; 174 | }; 175 | buildConfigurationList = 607FACCB1AFB9204008FA782 /* Build configuration list for PBXProject "NKButton" */; 176 | compatibilityVersion = "Xcode 3.2"; 177 | developmentRegion = English; 178 | hasScannedForEncodings = 0; 179 | knownRegions = ( 180 | English, 181 | en, 182 | Base, 183 | ); 184 | mainGroup = 607FACC71AFB9204008FA782; 185 | productRefGroup = 607FACD11AFB9204008FA782 /* Products */; 186 | projectDirPath = ""; 187 | projectRoot = ""; 188 | targets = ( 189 | 607FACCF1AFB9204008FA782 /* NKButton_Example */, 190 | ); 191 | }; 192 | /* End PBXProject section */ 193 | 194 | /* Begin PBXResourcesBuildPhase section */ 195 | 607FACCE1AFB9204008FA782 /* Resources */ = { 196 | isa = PBXResourcesBuildPhase; 197 | buildActionMask = 2147483647; 198 | files = ( 199 | 607FACDB1AFB9204008FA782 /* Main.storyboard in Resources */, 200 | 607FACE01AFB9204008FA782 /* LaunchScreen.xib in Resources */, 201 | 607FACDD1AFB9204008FA782 /* Images.xcassets in Resources */, 202 | ); 203 | runOnlyForDeploymentPostprocessing = 0; 204 | }; 205 | /* End PBXResourcesBuildPhase section */ 206 | 207 | /* Begin PBXShellScriptBuildPhase section */ 208 | 1B8A756768417BF0B6C49B76 /* [CP] Check Pods Manifest.lock */ = { 209 | isa = PBXShellScriptBuildPhase; 210 | buildActionMask = 2147483647; 211 | files = ( 212 | ); 213 | inputFileListPaths = ( 214 | ); 215 | inputPaths = ( 216 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 217 | "${PODS_ROOT}/Manifest.lock", 218 | ); 219 | name = "[CP] Check Pods Manifest.lock"; 220 | outputFileListPaths = ( 221 | ); 222 | outputPaths = ( 223 | "$(DERIVED_FILE_DIR)/Pods-NKButton_Example-checkManifestLockResult.txt", 224 | ); 225 | runOnlyForDeploymentPostprocessing = 0; 226 | shellPath = /bin/sh; 227 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 228 | showEnvVarsInLog = 0; 229 | }; 230 | 71F8A181F57D0E3F4AC62EED /* [CP] Embed Pods Frameworks */ = { 231 | isa = PBXShellScriptBuildPhase; 232 | buildActionMask = 2147483647; 233 | files = ( 234 | ); 235 | inputPaths = ( 236 | "${PODS_ROOT}/Target Support Files/Pods-NKButton_Example/Pods-NKButton_Example-frameworks.sh", 237 | "${BUILT_PRODUCTS_DIR}/FrameLayoutKit/FrameLayoutKit.framework", 238 | "${BUILT_PRODUCTS_DIR}/NKButton/NKButton.framework", 239 | "${BUILT_PRODUCTS_DIR}/NVActivityIndicatorView/NVActivityIndicatorView.framework", 240 | ); 241 | name = "[CP] Embed Pods Frameworks"; 242 | outputPaths = ( 243 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FrameLayoutKit.framework", 244 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/NKButton.framework", 245 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/NVActivityIndicatorView.framework", 246 | ); 247 | runOnlyForDeploymentPostprocessing = 0; 248 | shellPath = /bin/sh; 249 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-NKButton_Example/Pods-NKButton_Example-frameworks.sh\"\n"; 250 | showEnvVarsInLog = 0; 251 | }; 252 | /* End PBXShellScriptBuildPhase section */ 253 | 254 | /* Begin PBXSourcesBuildPhase section */ 255 | 607FACCC1AFB9204008FA782 /* Sources */ = { 256 | isa = PBXSourcesBuildPhase; 257 | buildActionMask = 2147483647; 258 | files = ( 259 | 607FACD81AFB9204008FA782 /* ViewController.swift in Sources */, 260 | 607FACD61AFB9204008FA782 /* AppDelegate.swift in Sources */, 261 | ); 262 | runOnlyForDeploymentPostprocessing = 0; 263 | }; 264 | /* End PBXSourcesBuildPhase section */ 265 | 266 | /* Begin PBXVariantGroup section */ 267 | 607FACD91AFB9204008FA782 /* Main.storyboard */ = { 268 | isa = PBXVariantGroup; 269 | children = ( 270 | 607FACDA1AFB9204008FA782 /* Base */, 271 | ); 272 | name = Main.storyboard; 273 | sourceTree = ""; 274 | }; 275 | 607FACDE1AFB9204008FA782 /* LaunchScreen.xib */ = { 276 | isa = PBXVariantGroup; 277 | children = ( 278 | 607FACDF1AFB9204008FA782 /* Base */, 279 | ); 280 | name = LaunchScreen.xib; 281 | sourceTree = ""; 282 | }; 283 | /* End PBXVariantGroup section */ 284 | 285 | /* Begin XCBuildConfiguration section */ 286 | 607FACED1AFB9204008FA782 /* Debug */ = { 287 | isa = XCBuildConfiguration; 288 | buildSettings = { 289 | ALWAYS_SEARCH_USER_PATHS = NO; 290 | APPLICATION_EXTENSION_API_ONLY = YES; 291 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 292 | CLANG_CXX_LIBRARY = "libc++"; 293 | CLANG_ENABLE_MODULES = YES; 294 | CLANG_ENABLE_OBJC_ARC = YES; 295 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 296 | CLANG_WARN_BOOL_CONVERSION = YES; 297 | CLANG_WARN_COMMA = YES; 298 | CLANG_WARN_CONSTANT_CONVERSION = YES; 299 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 300 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 301 | CLANG_WARN_EMPTY_BODY = YES; 302 | CLANG_WARN_ENUM_CONVERSION = YES; 303 | CLANG_WARN_INFINITE_RECURSION = YES; 304 | CLANG_WARN_INT_CONVERSION = YES; 305 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 306 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 307 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 308 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 309 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 310 | CLANG_WARN_STRICT_PROTOTYPES = YES; 311 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 312 | CLANG_WARN_UNREACHABLE_CODE = YES; 313 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 314 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 315 | COPY_PHASE_STRIP = NO; 316 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 317 | ENABLE_STRICT_OBJC_MSGSEND = YES; 318 | ENABLE_TESTABILITY = YES; 319 | GCC_C_LANGUAGE_STANDARD = gnu99; 320 | GCC_DYNAMIC_NO_PIC = NO; 321 | GCC_NO_COMMON_BLOCKS = YES; 322 | GCC_OPTIMIZATION_LEVEL = 0; 323 | GCC_PREPROCESSOR_DEFINITIONS = ( 324 | "DEBUG=1", 325 | "$(inherited)", 326 | ); 327 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 328 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 329 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 330 | GCC_WARN_UNDECLARED_SELECTOR = YES; 331 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 332 | GCC_WARN_UNUSED_FUNCTION = YES; 333 | GCC_WARN_UNUSED_VARIABLE = YES; 334 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 335 | MTL_ENABLE_DEBUG_INFO = YES; 336 | ONLY_ACTIVE_ARCH = YES; 337 | SDKROOT = iphoneos; 338 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 339 | SWIFT_VERSION = 4.0; 340 | }; 341 | name = Debug; 342 | }; 343 | 607FACEE1AFB9204008FA782 /* Release */ = { 344 | isa = XCBuildConfiguration; 345 | buildSettings = { 346 | ALWAYS_SEARCH_USER_PATHS = NO; 347 | APPLICATION_EXTENSION_API_ONLY = YES; 348 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 349 | CLANG_CXX_LIBRARY = "libc++"; 350 | CLANG_ENABLE_MODULES = YES; 351 | CLANG_ENABLE_OBJC_ARC = YES; 352 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 353 | CLANG_WARN_BOOL_CONVERSION = YES; 354 | CLANG_WARN_COMMA = YES; 355 | CLANG_WARN_CONSTANT_CONVERSION = YES; 356 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 357 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 358 | CLANG_WARN_EMPTY_BODY = YES; 359 | CLANG_WARN_ENUM_CONVERSION = YES; 360 | CLANG_WARN_INFINITE_RECURSION = YES; 361 | CLANG_WARN_INT_CONVERSION = YES; 362 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 363 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 364 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 365 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 366 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 367 | CLANG_WARN_STRICT_PROTOTYPES = YES; 368 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 369 | CLANG_WARN_UNREACHABLE_CODE = YES; 370 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 371 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 372 | COPY_PHASE_STRIP = NO; 373 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 374 | ENABLE_NS_ASSERTIONS = NO; 375 | ENABLE_STRICT_OBJC_MSGSEND = YES; 376 | GCC_C_LANGUAGE_STANDARD = gnu99; 377 | GCC_NO_COMMON_BLOCKS = YES; 378 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 379 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 380 | GCC_WARN_UNDECLARED_SELECTOR = YES; 381 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 382 | GCC_WARN_UNUSED_FUNCTION = YES; 383 | GCC_WARN_UNUSED_VARIABLE = YES; 384 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 385 | MTL_ENABLE_DEBUG_INFO = NO; 386 | SDKROOT = iphoneos; 387 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 388 | SWIFT_VERSION = 4.0; 389 | VALIDATE_PRODUCT = YES; 390 | }; 391 | name = Release; 392 | }; 393 | 607FACF01AFB9204008FA782 /* Debug */ = { 394 | isa = XCBuildConfiguration; 395 | baseConfigurationReference = E81A17708EA8EC32B91F40DC /* Pods-NKButton_Example.debug.xcconfig */; 396 | buildSettings = { 397 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 398 | CODE_SIGN_ENTITLEMENTS = NKButton_Example.entitlements; 399 | DEVELOPMENT_TEAM = 385YL4KG69; 400 | INFOPLIST_FILE = NKButton/Info.plist; 401 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 402 | "IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.2; 403 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 404 | MODULE_NAME = ExampleApp; 405 | PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)"; 406 | PRODUCT_NAME = "$(TARGET_NAME)"; 407 | SUPPORTS_MACCATALYST = YES; 408 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 409 | SWIFT_VERSION = 5.0; 410 | TARGETED_DEVICE_FAMILY = "1,2,6"; 411 | }; 412 | name = Debug; 413 | }; 414 | 607FACF11AFB9204008FA782 /* Release */ = { 415 | isa = XCBuildConfiguration; 416 | baseConfigurationReference = F8D16959655523722DDFA0A2 /* Pods-NKButton_Example.release.xcconfig */; 417 | buildSettings = { 418 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 419 | CODE_SIGN_ENTITLEMENTS = NKButton_Example.entitlements; 420 | DEVELOPMENT_TEAM = 385YL4KG69; 421 | INFOPLIST_FILE = NKButton/Info.plist; 422 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 423 | "IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.2; 424 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 425 | MODULE_NAME = ExampleApp; 426 | PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)"; 427 | PRODUCT_NAME = "$(TARGET_NAME)"; 428 | SUPPORTS_MACCATALYST = YES; 429 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 430 | SWIFT_VERSION = 5.0; 431 | TARGETED_DEVICE_FAMILY = "1,2,6"; 432 | }; 433 | name = Release; 434 | }; 435 | /* End XCBuildConfiguration section */ 436 | 437 | /* Begin XCConfigurationList section */ 438 | 607FACCB1AFB9204008FA782 /* Build configuration list for PBXProject "NKButton" */ = { 439 | isa = XCConfigurationList; 440 | buildConfigurations = ( 441 | 607FACED1AFB9204008FA782 /* Debug */, 442 | 607FACEE1AFB9204008FA782 /* Release */, 443 | ); 444 | defaultConfigurationIsVisible = 0; 445 | defaultConfigurationName = Release; 446 | }; 447 | 607FACEF1AFB9204008FA782 /* Build configuration list for PBXNativeTarget "NKButton_Example" */ = { 448 | isa = XCConfigurationList; 449 | buildConfigurations = ( 450 | 607FACF01AFB9204008FA782 /* Debug */, 451 | 607FACF11AFB9204008FA782 /* Release */, 452 | ); 453 | defaultConfigurationIsVisible = 0; 454 | defaultConfigurationName = Release; 455 | }; 456 | /* End XCConfigurationList section */ 457 | }; 458 | rootObject = 607FACC81AFB9204008FA782 /* Project object */; 459 | } 460 | -------------------------------------------------------------------------------- /NKButton/Classes/NKButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NKButton.swift 3 | // NKButton 4 | // 5 | // Created by Nam Kennic on 8/18/17. 6 | // Copyright © 2017 Nam Kennic. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import FrameLayoutKit 11 | #if canImport(NVActivityIndicatorView) 12 | import NVActivityIndicatorView 13 | #endif 14 | 15 | public extension UIControl.State { 16 | static let hovered = UIControl.State(rawValue: 1 << 18) 17 | } 18 | 19 | public enum NKButtonLoadingIndicatorAlignment { 20 | case left 21 | case center 22 | case right 23 | case atImage 24 | case atPosition(position: CGPoint) 25 | } 26 | 27 | public enum NKButtonImageAlignment { 28 | case left 29 | case right 30 | case top 31 | case bottom 32 | case leftEdge(spacing: CGFloat) 33 | case rightEdge(spacing: CGFloat) 34 | case topEdge(spacing: CGFloat) 35 | case bottomEdge(spacing: CGFloat) 36 | } 37 | 38 | open class NKButton: UIButton { 39 | 40 | /** Set/Get title of the button */ 41 | open var title: String? { 42 | get { currentTitle } 43 | set { 44 | setTitle(newValue, for: .normal) 45 | if state != .normal { setTitle(newValue, for: state) } 46 | 47 | setNeedsLayout() 48 | } 49 | } 50 | 51 | /** Space between image and text */ 52 | open var spacing: CGFloat { 53 | get { contentFrameLayout.spacing } 54 | set { 55 | contentFrameLayout.spacing = newValue 56 | setNeedsLayout() 57 | } 58 | } 59 | 60 | /** Minimum size of imageView, set zero to width or height to disable */ 61 | open var imageMinSize: CGSize { 62 | get { imageFrameLayout.minSize } 63 | set { 64 | imageFrameLayout.minSize = newValue 65 | setNeedsLayout() 66 | } 67 | } 68 | 69 | /** Maximum size of imageView, set zero to width or height to disable */ 70 | open var imageMaxSize: CGSize { 71 | get { imageFrameLayout.maxSize } 72 | set { 73 | imageFrameLayout.maxSize = newValue 74 | setNeedsLayout() 75 | } 76 | } 77 | 78 | /** Fixed size of imageView, set zero to width or height to disable */ 79 | open var imageFixedSize: CGSize { 80 | get {imageFrameLayout.fixedSize } 81 | set { 82 | imageFrameLayout.fixedSize = newValue 83 | setNeedsLayout() 84 | } 85 | } 86 | 87 | /** Extend size that will be included in sizeThatFits function */ 88 | open var extendSize: CGSize = .zero 89 | 90 | /** Corner Radius, will be ignored if `isRoundedButton` is true */ 91 | open var cornerRadius: CGFloat = 0 { 92 | didSet { 93 | guard cornerRadius != oldValue else { return } 94 | setNeedsDisplay() 95 | } 96 | } 97 | 98 | /** Shadow radius */ 99 | open var shadowRadius: CGFloat = 0 { 100 | didSet { 101 | guard shadowRadius != oldValue else { return } 102 | setNeedsDisplay() 103 | } 104 | } 105 | 106 | /** Shadow opacity */ 107 | open var shadowOpacity: Float = 0.5 { 108 | didSet { 109 | guard shadowOpacity != oldValue else { return } 110 | setNeedsDisplay() 111 | } 112 | } 113 | 114 | /** Shadow offset */ 115 | open var shadowOffset: CGSize = .zero { 116 | didSet { 117 | guard shadowOffset != oldValue else { return } 118 | setNeedsDisplay() 119 | } 120 | } 121 | 122 | /** Size of border */ 123 | open var borderSize: CGFloat { 124 | get { borderSize(for: .normal) } 125 | set { setBorderSize(newValue, for: .normal) } 126 | } 127 | 128 | /** Rounds both sides of the button */ 129 | open var isRoundedButton: Bool = false { 130 | didSet { 131 | guard isRoundedButton != oldValue else { return } 132 | setNeedsDisplay() 133 | } 134 | } 135 | 136 | /** If `true`, title label will not be underlined when `Settings > Accessibility > Button Shapes` is ON */ 137 | open var underlineTitleDisabled: Bool = false { 138 | didSet { 139 | guard underlineTitleDisabled != oldValue else { return } 140 | setNeedsDisplay() 141 | } 142 | } 143 | 144 | /** Image alignment */ 145 | open var imageAlignment: NKButtonImageAlignment = .left { 146 | didSet { 147 | updateLayoutAlignment() 148 | } 149 | } 150 | 151 | /** Text Horizontal Alignment */ 152 | open var textHorizontalAlignment: NKContentHorizontalAlignment { 153 | get { labelFrame.alignment.horizontal } 154 | set { 155 | labelFrame.alignment.horizontal = newValue 156 | setNeedsLayout() 157 | } 158 | } 159 | 160 | /** Text Vertical Alignment */ 161 | open var textVerticalAlignment: NKContentVerticalAlignment { 162 | get { labelFrame.alignment.vertical } 163 | set { 164 | labelFrame.alignment.vertical = newValue 165 | setNeedsLayout() 166 | } 167 | } 168 | 169 | /** Text Alignment */ 170 | open var textAlignment: (vertical: NKContentVerticalAlignment, horizontal: NKContentHorizontalAlignment) = (.center, .center) { 171 | didSet { 172 | labelFrame.alignment = textAlignment 173 | setNeedsLayout() 174 | } 175 | } 176 | 177 | override open var contentEdgeInsets: UIEdgeInsets { 178 | get { contentFrameLayout.edgeInsets } 179 | set { 180 | contentFrameLayout.edgeInsets = newValue 181 | setNeedsLayout() 182 | } 183 | } 184 | 185 | /** If `true`, disabled color will be set from normal color with tranparency */ 186 | open var autoSetDisableColor: Bool = true 187 | /** If `true`, highlighted color will be set from normal color with tranparency */ 188 | open var autoSetHighlightedColor: Bool = true 189 | 190 | open var flashColor: UIColor! = UIColor(white: 1.0, alpha: 0.5) { 191 | didSet { 192 | flashLayer.fillColor = flashColor.cgColor 193 | } 194 | } 195 | 196 | /** Set loading state. Tap interaction will be disabled while loading */ 197 | open var isLoading: Bool = false { 198 | didSet { 199 | guard isLoading != oldValue else { return } 200 | isEnabled = !isLoading 201 | 202 | if isLoading { 203 | showLoadingView() 204 | 205 | if transitionToCircleWhenLoading { 206 | titleLabel?.alpha = 0.0 207 | imageView?.alpha = 0.0 208 | transition(toCircle: true) 209 | } 210 | else { 211 | if hideImageWhileLoading { 212 | imageView?.alpha = 0.0 213 | } 214 | 215 | if hideTitleWhileLoading { 216 | titleLabel?.alpha = 0.0 217 | } 218 | } 219 | } 220 | else { 221 | hideLoadingView() 222 | 223 | if transitionToCircleWhenLoading { 224 | titleLabel?.alpha = 1.0 225 | imageView?.alpha = 1.0 226 | transition(toCircle: false) 227 | } 228 | else { 229 | if hideImageWhileLoading { 230 | imageView?.alpha = 1.0 231 | } 232 | 233 | if hideTitleWhileLoading { 234 | titleLabel?.alpha = 1.0 235 | } 236 | } 237 | } 238 | } 239 | } 240 | /// `true` is mous cursor is hovering 241 | public fileprivate(set) var isHovering = false 242 | /** imageView will be hidden when `isLoading` is true */ 243 | open var hideImageWhileLoading = false 244 | /** titleLabel will be hidden when `isLoading` is true */ 245 | open var hideTitleWhileLoading = true 246 | /** Button will animated to circle shape when set `isLoading = true`*/ 247 | open var transitionToCircleWhenLoading: Bool = false 248 | #if canImport(NVActivityIndicatorView) 249 | /** Style of loading indicator */ 250 | open var loadingIndicatorStyle: NVActivityIndicatorType = .ballPulse 251 | #else 252 | open var loadingIndicatorStyle: UIActivityIndicatorView.Style = .white 253 | #endif 254 | /** Scale ratio of loading indicator, based on the minimum value of button width or height */ 255 | open var loadingIndicatorScaleRatio: CGFloat = 0.7 256 | /** Color of loading indicator, if `nil`, it will use titleColor of normal state */ 257 | open var loadingIndicatorColor: UIColor? = nil 258 | /** Alignment for loading indicator */ 259 | open var loadingIndicatorAlignment: NKButtonLoadingIndicatorAlignment = .center 260 | 261 | private let flashAnimationKey = "flashAnimation" 262 | open var isFlashing: Bool { 263 | return flashLayer.animation(forKey: flashAnimationKey) != nil 264 | } 265 | 266 | /** The background view of the button */ 267 | open var backgroundView: UIView? = nil { 268 | didSet { 269 | oldValue?.layer.removeFromSuperlayer() 270 | guard let view = backgroundView else { return } 271 | view.isUserInteractionEnabled = false 272 | view.layer.masksToBounds = true 273 | layer.insertSublayer(view.layer, at: 0) 274 | setNeedsLayout() 275 | } 276 | } 277 | /** `FrameLayout` that layout imageView */ 278 | public let imageFrameLayout = FrameLayout() 279 | /** `FrameLayout` that handles textLabel */ 280 | public let labelFrameLayout = FrameLayout() 281 | /** `FrameLayout` that handles contents */ 282 | public let contentFrameLayout = DoubleFrameLayout(axis: .horizontal) 283 | 284 | #if canImport(NVActivityIndicatorView) 285 | fileprivate var loadingView : NVActivityIndicatorView? = nil 286 | #else 287 | fileprivate var loadingView : UIActivityIndicatorView? = nil 288 | #endif 289 | fileprivate let shadowLayer = CAShapeLayer() 290 | fileprivate let backgroundLayer = CAShapeLayer() 291 | fileprivate let flashLayer = CAShapeLayer() 292 | fileprivate let gradientLayer = CAGradientLayer() 293 | 294 | fileprivate var bgColorDict : [String : UIColor] = [:] 295 | fileprivate var borderColorDict : [String : UIColor] = [:] 296 | fileprivate var shadowColorDict : [String : UIColor] = [:] 297 | fileprivate var gradientColorDict : [String : [UIColor]] = [:] 298 | fileprivate var borderSizeDict : [String : CGFloat] = [:] 299 | fileprivate var borderDashDict : [String : [NSNumber]] = [:] 300 | fileprivate var titleFontDict : [String : UIFont] = [:] 301 | 302 | fileprivate var labelFrame: FrameLayout { contentFrameLayout.leftFrameLayout.targetView == labelFrameLayout ? contentFrameLayout.leftFrameLayout : contentFrameLayout.rightFrameLayout } 303 | 304 | // MARK: - 305 | 306 | public convenience init(title: String, titleColor: UIColor? = nil, buttonColor: UIColor? = nil, shadowColor: UIColor? = nil) { 307 | self.init() 308 | self.title = title 309 | 310 | if let color = titleColor { setTitleColor(color, for: .normal) } 311 | if let color = buttonColor { setBackgroundColor(color, for: .normal) } 312 | if let color = shadowColor { setShadowColor(color, for: .normal) } 313 | } 314 | 315 | public init() { 316 | super.init(frame: .zero) 317 | setupUI() 318 | } 319 | 320 | required public init?(coder aDecoder: NSCoder) { 321 | super.init(coder: aDecoder) 322 | setupUI() 323 | } 324 | 325 | open func setupUI() { 326 | flashLayer.opacity = 0 327 | flashLayer.fillColor = flashColor.cgColor 328 | contentEdgeInsets = .zero 329 | 330 | layer.addSublayer(shadowLayer) 331 | layer.addSublayer(backgroundLayer) 332 | layer.addSublayer(flashLayer) 333 | layer.addSublayer(gradientLayer) 334 | 335 | contentFrameLayout.isIntrinsicSizeEnabled = true 336 | contentFrameLayout.frameLayout1.alignment = (.center, .center) 337 | contentFrameLayout.frameLayout2.alignment = (.center, .center) 338 | 339 | imageFrameLayout.alignment = (.fit, .fit) 340 | imageFrameLayout.targetView = imageView 341 | 342 | labelFrameLayout.alignment = (.fill, .fill) 343 | labelFrameLayout.targetView = titleLabel 344 | 345 | updateLayoutAlignment() 346 | addSubview(labelFrameLayout) 347 | addSubview(imageFrameLayout) 348 | addSubview(contentFrameLayout) 349 | 350 | if #available(iOS 13.4, *) { 351 | enablePointerInteraction() 352 | } 353 | else if #available(iOS 13.0, *) { 354 | enableHoverGesture() 355 | } 356 | } 357 | 358 | @available(iOS 13.0, *) 359 | open func enableHoverGesture() { 360 | let hoverGesture = UIHoverGestureRecognizer(target: self, action: #selector(onHovered)) 361 | addGestureRecognizer(hoverGesture) 362 | } 363 | 364 | /* 365 | open override func setNeedsLayout() { 366 | super.setNeedsLayout() 367 | 368 | contentFrameLayout.setNeedsLayout() 369 | imageFrameLayout.setNeedsLayout() 370 | labelFrameLayout.setNeedsLayout() 371 | } 372 | */ 373 | 374 | override open func sizeThatFits(_ size: CGSize) -> CGSize { 375 | if let attributedText = attributedTitle(for: state) { 376 | titleLabel?.attributedText = attributedText 377 | } 378 | else { 379 | titleLabel?.text = title(for: state) 380 | } 381 | 382 | imageView?.image = image(for: state) 383 | 384 | var result = contentFrameLayout.sizeThatFits(size) 385 | 386 | result.width += extendSize.width 387 | result.height += extendSize.height 388 | result.width = min(result.width, size.width) 389 | result.height = min(result.height, size.height) 390 | 391 | return result 392 | } 393 | 394 | override open func sizeToFit() { 395 | let size = sizeThatFits(UIScreen.main.bounds.size) 396 | frame = CGRect(origin: frame.origin, size: size) 397 | } 398 | 399 | override open func draw(_ rect: CGRect) { 400 | super.draw(rect) 401 | 402 | let currentState = isHovering ? [state, .hovered] : state 403 | let backgroundFrame = bounds 404 | let fillColor = backgroundColor(for: currentState) ?? backgroundColor(for: state) ?? backgroundColor(for: .normal) 405 | let strokeColor = borderColor(for: currentState) 406 | let strokeSize = borderSize(for: currentState) 407 | let lineDashPattern = borderDashPattern(for: currentState) 408 | let roundedPath = UIBezierPath(roundedRect: backgroundFrame, cornerRadius: cornerRadius) 409 | let path = transitionToCircleWhenLoading && isLoading ? backgroundLayer.path : roundedPath.cgPath 410 | 411 | backgroundLayer.path = path 412 | backgroundLayer.fillColor = fillColor?.cgColor 413 | backgroundLayer.strokeColor = strokeColor?.cgColor 414 | backgroundLayer.lineWidth = strokeSize 415 | backgroundLayer.miterLimit = roundedPath.miterLimit 416 | backgroundLayer.lineDashPattern = lineDashPattern 417 | 418 | flashLayer.path = path 419 | flashLayer.fillColor = flashColor.cgColor 420 | 421 | if let shadowColor = shadowColor(for: currentState) { 422 | shadowLayer.isHidden = false 423 | shadowLayer.path = path 424 | shadowLayer.shadowPath = path 425 | shadowLayer.fillColor = shadowColor.cgColor 426 | shadowLayer.shadowColor = shadowColor.cgColor 427 | shadowLayer.shadowRadius = shadowRadius 428 | shadowLayer.shadowOpacity = shadowOpacity 429 | shadowLayer.shadowOffset = shadowOffset 430 | } 431 | else { 432 | shadowLayer.isHidden = true 433 | } 434 | 435 | if let gradientColors = gradientColor(for: currentState) { 436 | var colors: [CGColor] = [] 437 | for color in gradientColors { 438 | colors.append(color.cgColor) 439 | } 440 | 441 | gradientLayer.isHidden = false 442 | gradientLayer.cornerRadius = cornerRadius 443 | gradientLayer.shadowPath = path 444 | gradientLayer.colors = colors 445 | } 446 | else { 447 | gradientLayer.isHidden = true 448 | gradientLayer.colors = nil 449 | } 450 | 451 | if let titleFont = titleFont(for: currentState) { titleLabel?.font = titleFont } 452 | if underlineTitleDisabled { removeLabelUnderline() } 453 | } 454 | 455 | override open func layoutSubviews() { 456 | super.layoutSubviews() 457 | 458 | let viewSize = bounds.size 459 | 460 | shadowLayer.frame = bounds 461 | backgroundLayer.frame = bounds 462 | flashLayer.frame = bounds 463 | gradientLayer.frame = bounds 464 | 465 | contentFrameLayout.frame = bounds 466 | contentFrameLayout.layoutSubviews() 467 | makeTitleRealCenter() 468 | 469 | if let imageView = imageView { 470 | #if swift(>=4.2) 471 | bringSubviewToFront(imageView) 472 | #else 473 | bringSubview(toFront: imageView) 474 | #endif 475 | } 476 | 477 | if let loadingView = loadingView { 478 | var point = CGPoint(x: 0, y: viewSize.height/2) 479 | 480 | if transitionToCircleWhenLoading { 481 | point.x = viewSize.width/2 482 | } 483 | else { 484 | switch (loadingIndicatorAlignment) { 485 | case .left: point.x = loadingView.frame.size.width/2 + 5 + contentFrameLayout.edgeInsets.left 486 | case .center: point.x = viewSize.width/2 487 | case .right: point.x = viewSize.width - (loadingView.frame.size.width/2) - 5 - contentFrameLayout.edgeInsets.right 488 | case .atImage: point = imageView?.center ?? point 489 | case .atPosition(let position): point = position 490 | } 491 | } 492 | 493 | loadingView.center = point 494 | 495 | titleLabel?.alpha = hideTitleWhileLoading ? 0.0 : 1.0 496 | imageView?.alpha = hideImageWhileLoading ? 0.0 : 1.0 497 | } 498 | 499 | if isRoundedButton { 500 | cornerRadius = viewSize.height / 2 501 | setNeedsDisplay() 502 | } 503 | 504 | gradientLayer.cornerRadius = cornerRadius 505 | gradientLayer.masksToBounds = cornerRadius > 0 506 | 507 | backgroundView?.layer.cornerRadius = cornerRadius 508 | backgroundView?.frame = bounds 509 | } 510 | 511 | open override func didMoveToWindow() { 512 | super.didMoveToWindow() 513 | guard window != nil else { return } 514 | setNeedsLayout() 515 | } 516 | 517 | open override func didMoveToSuperview() { 518 | super.didMoveToSuperview() 519 | guard window != nil else { return } 520 | setNeedsLayout() 521 | } 522 | 523 | fileprivate func updateLayoutAlignment() { 524 | switch imageAlignment { 525 | case .left: 526 | contentFrameLayout.axis = .horizontal 527 | contentFrameLayout.distribution = .center 528 | 529 | contentFrameLayout.leftFrameLayout.targetView = imageFrameLayout 530 | contentFrameLayout.rightFrameLayout.targetView = labelFrameLayout 531 | break 532 | 533 | case .leftEdge(let spacing): 534 | contentFrameLayout.axis = .horizontal 535 | contentFrameLayout.distribution = .left 536 | 537 | imageFrameLayout.padding(top: 0, left: spacing, bottom: 0, right: 0) 538 | contentFrameLayout.leftFrameLayout.targetView = imageFrameLayout 539 | contentFrameLayout.rightFrameLayout.targetView = labelFrameLayout 540 | break 541 | 542 | case .right: 543 | contentFrameLayout.axis = .horizontal 544 | contentFrameLayout.distribution = .center 545 | 546 | contentFrameLayout.leftFrameLayout.targetView = labelFrameLayout 547 | contentFrameLayout.rightFrameLayout.targetView = imageFrameLayout 548 | break 549 | 550 | case .rightEdge(let spacing): 551 | contentFrameLayout.axis = .horizontal 552 | contentFrameLayout.distribution = .right 553 | 554 | imageFrameLayout.padding(top: 0, left: 0, bottom: 0, right: spacing) 555 | contentFrameLayout.leftFrameLayout.targetView = labelFrameLayout 556 | contentFrameLayout.rightFrameLayout.targetView = imageFrameLayout 557 | break 558 | 559 | case .top: 560 | contentFrameLayout.axis = .vertical 561 | contentFrameLayout.distribution = .center 562 | 563 | contentFrameLayout.topFrameLayout.targetView = imageFrameLayout 564 | contentFrameLayout.bottomFrameLayout.targetView = labelFrameLayout 565 | break 566 | 567 | case .topEdge(let spacing): 568 | contentFrameLayout.axis = .vertical 569 | contentFrameLayout.distribution = .top 570 | 571 | imageFrameLayout.padding(top: spacing, left: 0, bottom: 0, right: 0) 572 | contentFrameLayout.topFrameLayout.targetView = imageFrameLayout 573 | contentFrameLayout.bottomFrameLayout.targetView = labelFrameLayout 574 | break 575 | 576 | case .bottom: 577 | contentFrameLayout.axis = .vertical 578 | contentFrameLayout.distribution = .center 579 | 580 | contentFrameLayout.topFrameLayout.targetView = labelFrameLayout 581 | contentFrameLayout.bottomFrameLayout.targetView = imageFrameLayout 582 | break 583 | 584 | case .bottomEdge(let spacing): 585 | contentFrameLayout.axis = .vertical 586 | contentFrameLayout.distribution = .bottom 587 | 588 | imageFrameLayout.padding(top: 0, left: 0, bottom: spacing, right: 0) 589 | contentFrameLayout.topFrameLayout.targetView = labelFrameLayout 590 | contentFrameLayout.bottomFrameLayout.targetView = imageFrameLayout 591 | break 592 | } 593 | 594 | labelFrame.alignment = textAlignment 595 | setNeedsDisplay() 596 | setNeedsLayout() 597 | } 598 | 599 | fileprivate func makeTitleRealCenter() { 600 | guard imageView?.image != nil else { return } 601 | guard let titleLabel = titleLabel else { return } 602 | 603 | func alignCenter() { 604 | var labelBound = titleLabel.frame 605 | let contentBounds = bounds.inset(by: contentEdgeInsets) 606 | let textSize = titleLabel.sizeThatFits(contentBounds.size) 607 | labelBound.origin.x = contentEdgeInsets.left + (contentBounds.size.width - textSize.width)/2 608 | titleLabel.frame = labelBound 609 | } 610 | 611 | if textHorizontalAlignment == .center { 612 | switch imageAlignment { 613 | case .leftEdge(_): 614 | alignCenter() 615 | if let imageView = imageView, imageView.frame.maxX > titleLabel.frame.minX { titleLabel.frame.origin.x = imageView.frame.maxX + spacing } 616 | 617 | case .rightEdge(_): 618 | alignCenter() 619 | if let imageView = imageView, titleLabel.frame.maxX > imageView.frame.minX { titleLabel.frame.origin.x -= (titleLabel.frame.maxX - imageView.frame.minX) + spacing } 620 | 621 | default: break 622 | } 623 | } 624 | } 625 | 626 | // MARK: - 627 | 628 | override open var frame: CGRect { 629 | didSet { 630 | setNeedsDisplay() 631 | setNeedsLayout() 632 | } 633 | } 634 | 635 | override open var bounds: CGRect { 636 | didSet { 637 | setNeedsDisplay() 638 | setNeedsLayout() 639 | } 640 | } 641 | 642 | override open var center: CGPoint { 643 | didSet { 644 | setNeedsDisplay() 645 | setNeedsLayout() 646 | } 647 | } 648 | 649 | override open var isHighlighted: Bool { 650 | didSet { 651 | guard isHighlighted != oldValue else { return } 652 | setNeedsDisplay() 653 | 654 | // #if os(iOS) 655 | // if isHighlighted { 656 | // if #available(iOS 10, *) { 657 | // let generator = UIImpactFeedbackGenerator(style: .light) 658 | // generator.prepare() 659 | // generator.impactOccurred() 660 | // } 661 | // } 662 | // #endif 663 | } 664 | } 665 | 666 | @available(iOS 13.0, *) 667 | @objc func onHovered(_ gesture: UIHoverGestureRecognizer) { 668 | let gestureState = gesture.state 669 | if gestureState == .began || gestureState == .ended || gestureState == .cancelled { 670 | isHovering = gestureState == .began 671 | setNeedsDisplay() 672 | } 673 | } 674 | 675 | 676 | // MARK: - 677 | 678 | open func startFlashing(flashDuration: TimeInterval = 0.5, intensity: Float = 0.85, repeatCount: Int = -1) { 679 | flashLayer.removeAnimation(forKey: flashAnimationKey) 680 | 681 | let flash = CABasicAnimation(keyPath: "opacity") 682 | flash.fromValue = 0.0 683 | flash.toValue = intensity 684 | flash.duration = flashDuration 685 | flash.autoreverses = true 686 | flash.repeatCount = repeatCount < 0 ? .infinity : Float(repeatCount) 687 | flashLayer.add(flash, forKey: flashAnimationKey) 688 | } 689 | 690 | open func stopFlashing() { 691 | flashLayer.removeAnimation(forKey: flashAnimationKey) 692 | } 693 | 694 | @available(iOS 13.4, *) 695 | open func enablePointerInteraction(insets: CGFloat = -5) { 696 | isPointerInteractionEnabled = true 697 | pointerStyleProvider = { (button, effect, shape) in 698 | let frame = button.frame.insetBy(dx: insets, dy: insets) 699 | let buttonShape = UIPointerShape.roundedRect(frame, radius: self.cornerRadius) 700 | return UIPointerStyle(effect: effect, shape: buttonShape) 701 | } 702 | } 703 | 704 | // MARK: - 705 | 706 | override open func setTitle(_ title: String?, for state: UIControl.State) { 707 | super.setTitle(title, for: state) 708 | guard self.state == state else { return } 709 | titleLabel?.text = title 710 | setNeedsLayout() 711 | } 712 | 713 | open func setTitleFont(_ font: UIFont?, for state: UIControl.State) { 714 | let key = titleFontKey(for: state) 715 | titleFontDict[key] = font 716 | guard self.state == state else { return } 717 | titleLabel?.font = font 718 | setNeedsLayout() 719 | } 720 | 721 | override open func setImage(_ image: UIImage?, for state: UIControl.State) { 722 | super.setImage(image, for: state) 723 | guard self.state == state else { return } 724 | imageView?.image = image 725 | setNeedsLayout() 726 | } 727 | 728 | open func setBackgroundColor(_ color: UIColor?, for state: UIControl.State) { 729 | let key = backgroundColorKey(for: state) 730 | bgColorDict[key] = color 731 | setNeedsDisplay() 732 | } 733 | 734 | open func setBorderColor(_ color: UIColor?, for state: UIControl.State) { 735 | let key = borderColorKey(for: state) 736 | borderColorDict[key] = color 737 | setNeedsDisplay() 738 | } 739 | 740 | open func setShadowColor(_ color: UIColor?, for state: UIControl.State) { 741 | let key = shadowColorKey(for: state) 742 | shadowColorDict[key] = color 743 | setNeedsDisplay() 744 | } 745 | 746 | open func setGradientColor(_ colors: [UIColor]?, for state: UIControl.State) { 747 | let key = gradientColorKey(for: state) 748 | gradientColorDict[key] = colors 749 | setNeedsDisplay() 750 | } 751 | 752 | open func setBorderSize(_ value: CGFloat?, for state: UIControl.State) { 753 | let key = borderSizeKey(for: state) 754 | borderSizeDict[key] = value 755 | setNeedsDisplay() 756 | } 757 | 758 | open func setBorderDashPattern(_ value: [NSNumber]?, for state: UIControl.State) { 759 | let key = borderDashKey(for: state) 760 | borderDashDict[key] = value 761 | setNeedsDisplay() 762 | } 763 | 764 | open func backgroundColor(for state: UIControl.State) -> UIColor? { 765 | let key = backgroundColorKey(for: state) 766 | var result = bgColorDict[key] 767 | 768 | if result == nil { 769 | if state == .disabled && autoSetDisableColor { 770 | let normalColor = backgroundColor(for: .normal) 771 | result = normalColor != nil ? normalColor!.withAlphaComponent(0.3) : nil 772 | } 773 | else if state == .highlighted && autoSetHighlightedColor { 774 | let normalColor = backgroundColor(for: .normal) 775 | result = normalColor != nil ? normalColor!.darker(by: 0.5) : nil 776 | } 777 | } 778 | 779 | return result 780 | } 781 | 782 | open func borderColor(for state: UIControl.State) -> UIColor? { 783 | let key = borderColorKey(for: state) 784 | var result = borderColorDict[key] 785 | 786 | if result == nil { 787 | if state == .disabled && autoSetDisableColor { 788 | let normalColor = borderColor(for: .normal) 789 | result = normalColor != nil ? normalColor!.withAlphaComponent(0.3) : nil 790 | } 791 | else if state == .highlighted && autoSetHighlightedColor { 792 | let normalColor = borderColor(for: .normal) 793 | result = normalColor != nil ? normalColor!.darker(by: 0.5) : nil 794 | } 795 | } 796 | 797 | return result 798 | } 799 | 800 | open func borderDashPattern(for state: UIControl.State) -> [NSNumber]? { 801 | let key = borderDashKey(for: state) 802 | return borderDashDict[key] 803 | } 804 | 805 | open func shadowColor(for state: UIControl.State) -> UIColor? { 806 | let key = shadowColorKey(for: state) 807 | return shadowColorDict[key] 808 | } 809 | 810 | open func gradientColor(for state: UIControl.State) -> [UIColor]? { 811 | let key = gradientColorKey(for: state) 812 | return gradientColorDict[key] 813 | } 814 | 815 | open func borderSize(for state: UIControl.State) -> CGFloat { 816 | let key = borderSizeKey(for: state) 817 | return borderSizeDict[key] ?? 0 818 | } 819 | 820 | open func titleFont(for state: UIControl.State) -> UIFont? { 821 | let key = titleFontKey(for: state) 822 | return titleFontDict[key] 823 | } 824 | 825 | // MARK: - 826 | 827 | fileprivate func backgroundColorKey(for state: UIControl.State) -> String { 828 | return "bg\(state.rawValue)" 829 | } 830 | 831 | fileprivate func borderColorKey(for state: UIControl.State) -> String { 832 | return "br\(state.rawValue)" 833 | } 834 | 835 | fileprivate func shadowColorKey(for state: UIControl.State) -> String { 836 | return "sd\(state.rawValue)" 837 | } 838 | 839 | fileprivate func gradientColorKey(for state: UIControl.State) -> String { 840 | return "gr\(state.rawValue)" 841 | } 842 | 843 | fileprivate func borderSizeKey(for state: UIControl.State) -> String { 844 | return "bs\(state.rawValue)" 845 | } 846 | 847 | fileprivate func borderDashKey(for state: UIControl.State) -> String { 848 | return "bd\(state.rawValue)" 849 | } 850 | 851 | fileprivate func titleFontKey(for state: UIControl.State) -> String { 852 | return "tf\(state.rawValue)" 853 | } 854 | 855 | // MARK: - 856 | 857 | fileprivate func showLoadingView() { 858 | guard loadingView == nil else { return } 859 | 860 | let viewSize = bounds.size 861 | let minSize = min(viewSize.width, viewSize.height) * loadingIndicatorScaleRatio 862 | let indicatorSize = CGSize(width: minSize, height: minSize) 863 | let loadingFrame = CGRect(x: 0, y: 0, width: indicatorSize.width, height: indicatorSize.height) 864 | let color = loadingIndicatorColor ?? titleColor(for: .normal) 865 | 866 | #if canImport(NVActivityIndicatorView) 867 | loadingView = NVActivityIndicatorView(frame: loadingFrame, type: loadingIndicatorStyle, color: color, padding: 0) 868 | #else 869 | loadingView = UIActivityIndicatorView(style: loadingIndicatorStyle) 870 | #endif 871 | 872 | loadingView!.startAnimating() 873 | addSubview(loadingView!) 874 | setNeedsLayout() 875 | } 876 | 877 | fileprivate func hideLoadingView() { 878 | loadingView?.stopAnimating() 879 | loadingView?.removeFromSuperview() 880 | loadingView = nil 881 | } 882 | 883 | fileprivate func transition(toCircle: Bool) { 884 | backgroundLayer.removeAllAnimations() 885 | shadowLayer.removeAllAnimations() 886 | 887 | let animation = CABasicAnimation(keyPath: "bounds.size.width") 888 | 889 | if toCircle { 890 | animation.fromValue = frame.width 891 | animation.toValue = frame.height 892 | animation.duration = 0.1 893 | #if swift(>=4.2) 894 | animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut) 895 | #else 896 | animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut) 897 | #endif 898 | backgroundLayer.masksToBounds = true 899 | backgroundLayer.cornerRadius = min(frame.width, frame.height)/2 900 | } 901 | else { 902 | animation.fromValue = frame.height 903 | animation.toValue = frame.width 904 | animation.duration = 0.15 905 | #if swift(>=4.2) 906 | animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut) 907 | #else 908 | animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut) 909 | #endif 910 | 911 | setNeedsLayout() 912 | setNeedsDisplay() 913 | } 914 | 915 | #if swift(>=4.2) 916 | animation.fillMode = CAMediaTimingFillMode.forwards 917 | #else 918 | animation.fillMode = kCAFillModeForwards 919 | #endif 920 | animation.isRemovedOnCompletion = false 921 | 922 | backgroundLayer.add(animation, forKey: animation.keyPath) 923 | shadowLayer.add(animation, forKey: animation.keyPath) 924 | gradientLayer.add(animation, forKey: animation.keyPath) 925 | flashLayer.add(animation, forKey: animation.keyPath) 926 | } 927 | 928 | fileprivate func removeLabelUnderline() { 929 | guard let attributedText = titleLabel?.attributedText?.mutableCopy() as? NSMutableAttributedString else { return } 930 | attributedText.addAttribute(NSAttributedString.Key.underlineStyle, value: (0), range: NSRange(location: 0, length: attributedText.length)) 931 | titleLabel?.attributedText = attributedText 932 | } 933 | 934 | deinit { 935 | backgroundLayer.removeAllAnimations() 936 | shadowLayer.removeAllAnimations() 937 | gradientLayer.removeAllAnimations() 938 | flashLayer.removeAllAnimations() 939 | } 940 | 941 | } 942 | 943 | 944 | // MARK: - 945 | 946 | fileprivate extension UIColor { 947 | 948 | func lighter(by value:CGFloat = 0.5) -> UIColor? { 949 | return adjust(by: abs(value) ) 950 | } 951 | 952 | func darker(by value:CGFloat = 0.5) -> UIColor? { 953 | return adjust(by: -1 * abs(value) ) 954 | } 955 | 956 | func adjust(by value:CGFloat = 0.5) -> UIColor? { 957 | var r: CGFloat = 0, g: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0 958 | 959 | if getRed(&r, green: &g, blue: &b, alpha: &a) { 960 | return UIColor(red: min(r + value, 1.0), 961 | green: min(g + value, 1.0), 962 | blue: min(b + value, 1.0), 963 | alpha: a) 964 | } 965 | else { 966 | return nil 967 | } 968 | } 969 | 970 | } 971 | 972 | /** 973 | Supports: 974 | let button = NKButton() 975 | button.titles[.normal] = "" 976 | button.titleColors[[.normal, .highlighted]] = .black 977 | button.backgroundColors[[.normal, .highlighted]] = .white 978 | */ 979 | public class UIControlStateValue { 980 | private let getter: (UIControl.State) -> T? 981 | private let setter: (T?, UIControl.State) -> Void 982 | 983 | // The initializer is fileprivate here because all 984 | // extensions are in a single file. If it's split 985 | // in multiple files, this should be internal 986 | fileprivate init(getter: @escaping (UIControl.State) -> T?, 987 | setter: @escaping (T?, UIControl.State) -> Void) { 988 | self.getter = getter 989 | self.setter = setter 990 | } 991 | 992 | public subscript(state: UIControl.State) -> T? { 993 | get { getter(state) } 994 | set { setter(newValue, state) } 995 | } 996 | } 997 | 998 | public extension NKButton { 999 | 1000 | var attributedTitles: UIControlStateValue { UIControlStateValue(getter: self.attributedTitle(for:), setter: self.setAttributedTitle(_:for:)) } 1001 | var titles: UIControlStateValue { UIControlStateValue(getter: self.title(for:), setter: self.setTitle(_:for:)) } 1002 | var titleColors: UIControlStateValue { UIControlStateValue(getter: self.titleColor(for:), setter: self.setTitleColor(_:for:)) } 1003 | var titleFonts: UIControlStateValue { UIControlStateValue(getter: self.titleFont(for:), setter: self.setTitleFont(_:for:)) } 1004 | var images: UIControlStateValue { UIControlStateValue.init(getter: self.image, setter: self.setImage(_:for:)) } 1005 | var backgroundColors: UIControlStateValue { UIControlStateValue(getter: self.backgroundColor(for:), setter: self.setBackgroundColor(_:for:)) } 1006 | var borderColors: UIControlStateValue { UIControlStateValue(getter: self.borderColor(for:), setter: self.setBorderColor(_:for:)) } 1007 | var borderSizes: UIControlStateValue { UIControlStateValue(getter: self.borderSize(for:), setter: self.setBorderSize(_:for:)) } 1008 | var borderDashPatterns: UIControlStateValue<[NSNumber]> { UIControlStateValue<[NSNumber]>(getter: self.borderDashPattern(for:), setter: self.setBorderDashPattern(_:for:)) } 1009 | var shadowColors: UIControlStateValue { UIControlStateValue(getter: self.shadowColor(for:), setter: self.setShadowColor(_:for:)) } 1010 | var gradientColors: UIControlStateValue<[UIColor]> { UIControlStateValue<[UIColor]>(getter: self.gradientColor(for:), setter: self.setGradientColor(_:for:)) } 1011 | 1012 | } 1013 | --------------------------------------------------------------------------------