├── 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 | [](http://cocoapods.org/pods/NKButton)
4 | [](http://cocoapods.org/pods/NKButton)
5 | [](http://cocoapods.org/pods/NKButton)
6 | 
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 | 
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 |
--------------------------------------------------------------------------------