├── README.md ├── ExtensionKit.xcodeproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcshareddata │ └── xcschemes │ └── ExtensionKit.xcscheme ├── Sources ├── Objc │ ├── ObjcSwizzle.h │ └── ObjcSwizzle.m ├── Info.plist ├── Extension │ ├── Timer+Extension.swift │ ├── UITabBar+Extension.swift │ ├── DispatchQueue+Extension.swift │ ├── UITableView+Extension.swift │ ├── Bundle+Extension.swift │ ├── UIScreen+Extension.swift │ ├── NSError+Extension.swift │ ├── Data+Extension.swift │ ├── UINavigationBar+Extension.swift │ ├── UILabel+Extension.swift │ ├── UITextView+Extension.swift │ ├── UIDevice+Extension.swift │ ├── UIResponder+Extension.swift │ ├── UserDefaults+Extension.swift │ ├── NSObject+Extension.swift │ ├── UITextField+Extension.swift │ ├── Dictionary+Extension.swift │ ├── UIBarButtonItem+Extension.swift │ ├── UIApplication+Extension.swift │ ├── Array+Extension.swift │ ├── Number+Extension.swift │ ├── CALayer+Extension.swift │ ├── UIColor+Extension.swift │ ├── UIFont+Extension.swift │ ├── UIScrollView+Extension.swift │ ├── UIControl+Extension.swift │ ├── Lang+Extension.swift │ ├── UIButton+Extension.swift │ ├── UIViewController+Extension.swift │ ├── String+Extension.swift │ ├── UIImage+Extension.swift │ └── CoreGraphics+Extension.swift ├── Classes │ ├── NoPasteTextField.swift │ ├── Observable.swift │ ├── DashlineView.swift │ ├── CustomHeightNavigationBar.swift │ ├── TextObserver.swift │ ├── PlaceholderTextView.swift │ ├── CGD.swift │ └── BadgeView.swift ├── ExtensionKit.h └── Protocols │ └── ExtensionProtocols.swift ├── .gitignore ├── LICENSE └── ExtensionKit └── Sample └── ViewController.swift /README.md: -------------------------------------------------------------------------------- 1 | # ExtensionKit 2 | The extension utils code for swift on app development. 3 | 4 | The vendors extension split to [VendorsExtension](https://github.com/cuzv/VendorsExtension) 5 | -------------------------------------------------------------------------------- /ExtensionKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Sources/Objc/ObjcSwizzle.h: -------------------------------------------------------------------------------- 1 | // 2 | // ObjcSwizzle.h 3 | // ExtensionKit 4 | // 5 | // Created by Moch Xiao on 3/31/17. 6 | // Copyright © 2017 Moch. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface UIView (ObjcSwizzle) 12 | @end 13 | 14 | @interface UILabel (ObjcSwizzle) 15 | @end 16 | 17 | @interface UITextField (ObjcSwizzle) 18 | @end 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | build/ 4 | *.pbxuser 5 | !default.pbxuser 6 | *.mode1v3 7 | !default.mode1v3 8 | *.mode2v3 9 | !default.mode2v3 10 | *.perspectivev3 11 | !default.perspectivev3 12 | xcuserdata 13 | *.xccheckout 14 | *.moved-aside 15 | DerivedData 16 | *.hmap 17 | *.ipa 18 | *.xcuserstate 19 | 20 | # CocoaPods 21 | # 22 | # We recommend against adding the Pods directory to your .gitignore. However 23 | # you should judge for yourself, the pros and cons are mentioned at: 24 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control 25 | # 26 | Pods/ 27 | .DS_Store 28 | .svn/ 29 | xcuserdata/ 30 | -------------------------------------------------------------------------------- /Sources/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 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 snownothing 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Sources/Extension/Timer+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Timer+Extension.swift 3 | // ExtensionKit 4 | // 5 | // Created by Haioo Inc on 4/7/17. 6 | // Copyright © 2017 Moch. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public extension Timer { 12 | public func resume(after duration: TimeInterval = 0) { 13 | fireDate = Date(timeIntervalSinceNow: duration) 14 | } 15 | 16 | public func pause(after duration: TimeInterval = 0) { 17 | if 0 <= duration { 18 | fireDate = Date.distantFuture 19 | } else { 20 | DispatchQueue.main.asyncAfter(deadline: .now() + duration) { 21 | self.fireDate = Date.distantFuture 22 | } 23 | } 24 | } 25 | } 26 | 27 | public extension CADisplayLink { 28 | public func resume(after duration: TimeInterval = 0) { 29 | if 0 <= duration { 30 | isPaused = false 31 | } else { 32 | DispatchQueue.main.asyncAfter(deadline: .now() + duration) { 33 | self.isPaused = false 34 | } 35 | } 36 | } 37 | 38 | public func pause(after duration: TimeInterval = 0) { 39 | if 0 <= duration { 40 | isPaused = true 41 | } else { 42 | DispatchQueue.main.asyncAfter(deadline: .now() + duration) { 43 | self.isPaused = true 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Sources/Classes/NoPasteTextField.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NoPasteTextField.swift 3 | // Copyright (c) 2015-2016 Red Rain (http://mochxiao.com). 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | // THE SOFTWARE. 22 | // 23 | 24 | import UIKit 25 | 26 | final public class NoPasteTextField: UITextField { 27 | public override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool { 28 | UIMenuController.shared.isMenuVisible = false 29 | return false 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/Extension/UITabBar+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UITabBar+Extension.swift 3 | // Copyright (c) 2015-2016 Red Rain (http://mochxiao.com). 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | // THE SOFTWARE. 22 | // 23 | 24 | import UIKit 25 | 26 | public extension UITabBar { 27 | public var shimShadowView: UIView? { 28 | if let 29 | _backgroundView = value(forKey: "_backgroundView") as? NSObject, 30 | let _shadowView = _backgroundView.value(forKey: "_shadowView") as? UIView 31 | { 32 | return _shadowView 33 | } 34 | return nil 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/ExtensionKit.h: -------------------------------------------------------------------------------- 1 | // 2 | // ExtensionKit.h 3 | // Copyright (c) 2015-2016 Red Rain (http://mochxiao.com). 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | // THE SOFTWARE. 22 | // 23 | 24 | #import 25 | 26 | //! Project version number for ExtensionKit. 27 | FOUNDATION_EXPORT double ExtensionKitVersionNumber; 28 | 29 | //! Project version string for ExtensionKit. 30 | FOUNDATION_EXPORT const unsigned char ExtensionKitVersionString[]; 31 | 32 | // In this header, you should import all the public headers of your framework using statements like #import 33 | 34 | #import 35 | 36 | -------------------------------------------------------------------------------- /Sources/Classes/Observable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Observable.swift 3 | // Copyright (c) 2015-2016 Red Rain (http://mochxiao.com). 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | // THE SOFTWARE. 22 | // 23 | 24 | import Foundation 25 | 26 | open class Observable { 27 | public typealias Observer = (T) -> () 28 | fileprivate var observer: Observer? 29 | fileprivate var value: T { 30 | didSet { 31 | observer?(value) 32 | } 33 | } 34 | 35 | init(_ v: T) { 36 | value = v 37 | } 38 | 39 | open func observe(_ observer: Observer?) { 40 | self.observer = observer 41 | observer?(value) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/Extension/DispatchQueue+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DispatchQueue+Extension.swift 3 | // ExtensionKit 4 | // 5 | // Copyright (c) 2015-2016 Red Rain (http://mochxiao.com). 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | // 25 | 26 | import Foundation 27 | 28 | public extension DispatchQueue { 29 | private static var _onceTracker = [String]() 30 | 31 | public class func once(token: String, block: ()-> ()) { 32 | objc_sync_enter(self) 33 | defer { objc_sync_exit(self) } 34 | 35 | if _onceTracker.contains(token) { 36 | return 37 | } 38 | 39 | _onceTracker.append(token) 40 | 41 | block() 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/Extension/UITableView+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UITableView+Extension.swift 3 | // Copyright (c) 2015-2016 Red Rain (http://mochxiao.com). 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | // THE SOFTWARE. 22 | // 23 | 24 | import UIKit 25 | 26 | public extension UITableView { 27 | public func scrollToTop(animated: Bool = true) { 28 | if let indexPath = indexPathForRow(at: CGPoint(x: bounds.midX, y: 2)) , !isEmpty { 29 | scrollToRow(at: indexPath, at: .top, animated: animated) 30 | } 31 | } 32 | 33 | public var isEmpty: Bool { 34 | var count = 0 35 | for section in 0 ..< numberOfSections { 36 | count += numberOfRows(inSection: section) 37 | } 38 | return count == 0 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/Extension/Bundle+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Bundle+Extension.swift 3 | // Copyright (c) 2015-2016 Red Rain (http://mochxiao.com). 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | // THE SOFTWARE. 22 | // 23 | 24 | import Foundation 25 | 26 | public extension Bundle { 27 | fileprivate static let _mainBundle = Bundle.main 28 | 29 | public class var build: String { 30 | return _mainBundle.infoDictionary?["CFBundleVersion"] as? String ?? "" 31 | } 32 | 33 | public class var ver: String { 34 | return _mainBundle.infoDictionary?["CFBundleShortVersionString"] as? String ?? "" 35 | } 36 | 37 | public class var displayName: String { 38 | return _mainBundle.infoDictionary?["CFBundleDisplayName"] as? String ?? "" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/Extension/UIScreen+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIScreen+Extension.swift 3 | // Copyright (c) 2015-2016 Red Rain (http://mochxiao.com). 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | // THE SOFTWARE. 22 | // 23 | 24 | import UIKit 25 | 26 | public extension UIScreen { 27 | static let _width = UIScreen.main.bounds.size.width 28 | public class var width: CGFloat { 29 | return _width 30 | } 31 | 32 | static let _height = UIScreen.main.bounds.size.height 33 | public class var height: CGFloat { 34 | return _height 35 | } 36 | 37 | static let _scale = UIScreen.main.scale 38 | public class var scaleValue: CGFloat { 39 | return _scale 40 | } 41 | 42 | static let _size = UIScreen.main.bounds.size 43 | public class var size: CGSize { 44 | return _size 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Sources/Extension/NSError+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSError+Extension.swift 3 | // Copyright (c) 2015-2016 Red Rain (http://mochxiao.com). 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | // THE SOFTWARE. 22 | // 23 | 24 | import Foundation 25 | 26 | public extension NSError { 27 | public class var emptyErrorDomain: String { return "com.mochxiao.error.default" } 28 | public class var emptyErrorCode: Int { return 1024 } 29 | public class var empty: NSError { 30 | return NSError(domain: emptyErrorDomain, code: emptyErrorCode, userInfo: nil) 31 | } 32 | 33 | public class func make(message: String, code: Int = 9999) -> NSError { 34 | return NSError( 35 | domain: "com.mochxiao.error.maker", 36 | code: code, 37 | userInfo: [NSLocalizedDescriptionKey: message, NSLocalizedFailureReasonErrorKey: message] 38 | ) 39 | } 40 | } 41 | 42 | public func NSErrorFrom(message: String, code: Int = 9999) -> NSError { 43 | return NSError.make(message: message, code: code) 44 | } 45 | -------------------------------------------------------------------------------- /ExtensionKit/Sample/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Copyright (c) 2015-2016 Moch Xiao (http://mochxiao.com). 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | // THE SOFTWARE. 22 | // 23 | 24 | import UIKit 25 | 26 | class ViewController: UIViewController { 27 | 28 | override func viewDidLoad() { 29 | super.viewDidLoad() 30 | // Do any additional setup after loading the view, typically from a nib. 31 | 32 | let value = "value" 33 | UserDefaults["some"] = value 34 | 35 | } 36 | 37 | override func didReceiveMemoryWarning() { 38 | super.didReceiveMemoryWarning() 39 | // Dispose of any resources that can be recreated. 40 | } 41 | 42 | 43 | } 44 | 45 | 46 | enum Color { 47 | case Red 48 | case Green 49 | 50 | var rawValue: UIColor { 51 | switch self { 52 | case .Red: return UIColor.redColor() 53 | case .Green: return UIColor.greenColor() 54 | } 55 | } 56 | } 57 | 58 | let color = Color.Red.rawValue 59 | 60 | -------------------------------------------------------------------------------- /Sources/Extension/Data+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Data+Extension.swift 3 | // Copyright (c) 2015-2016 Red Rain (http://mochxiao.com). 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | // THE SOFTWARE. 22 | // 23 | 24 | import Foundation 25 | 26 | public extension Data { 27 | /// Create a Foundation object from JSON data. 28 | public var JSONObject: AnyObject? { 29 | do { 30 | return try JSONSerialization.jsonObject(with: self, options: []) as AnyObject 31 | } catch let error as NSError { 32 | logging("Deserialized JSON string failed with error: \(error)") 33 | return nil 34 | } 35 | } 36 | 37 | /// Generate JSON data from a Foundation object 38 | public static func make(fromJSONObject obj: AnyObject) -> Data? { 39 | do { 40 | return try JSONSerialization.data(withJSONObject: obj, options: []) 41 | } catch let error as NSError { 42 | logging("Serialized JSON string failed with error: \(error)") 43 | return nil 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Sources/Extension/UINavigationBar+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UINavigationBar+Extension.swift 3 | // Copyright (c) 2015-2016 Red Rain (http://mochxiao.com). 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | // THE SOFTWARE. 22 | // 23 | 24 | import UIKit 25 | 26 | // MARK: - Visible & Invisible 27 | 28 | public extension UINavigationBar { 29 | /// Only effect when `translucent` is true. 30 | public func setBackgroundVisible(_ visible: Bool) { 31 | if !isTranslucent { 32 | logging("`translucent` must be true if you wanna change background visible.") 33 | } 34 | 35 | setBackgroundImage(visible ? nil : UIImage(), for: .default) 36 | shadowImage = visible ? nil : UIImage() 37 | } 38 | } 39 | 40 | // MARK: - Hairline 41 | 42 | public extension UINavigationBar { 43 | public var shimShadowView: UIView? { 44 | if let 45 | _backgroundView = value(forKey: "_backgroundView") as? NSObject, 46 | let _shadowView = _backgroundView.value(forKey: "_shadowView") as? UIView 47 | { 48 | return _shadowView 49 | } 50 | return nil 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/Objc/ObjcSwizzle.m: -------------------------------------------------------------------------------- 1 | // 2 | // ObjcSwizzle.m 3 | // ExtensionKit 4 | // 5 | // Created by Moch Xiao on 3/31/17. 6 | // Copyright © 2017 Moch. All rights reserved. 7 | // 8 | 9 | #import "ObjcSwizzle.h" 10 | #import 11 | 12 | void EKSwizzleInstanceMethod(Class _Nonnull clazz, SEL _Nonnull originalSelector, SEL _Nonnull overrideSelector) { 13 | Method originalMethod = class_getInstanceMethod(clazz, originalSelector); 14 | Method overrideMethod = class_getInstanceMethod(clazz, overrideSelector); 15 | 16 | if (class_addMethod(clazz, originalSelector, method_getImplementation(overrideMethod), method_getTypeEncoding(overrideMethod))) { 17 | class_replaceMethod(clazz, overrideSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); 18 | } else { 19 | method_exchangeImplementations(originalMethod, overrideMethod); 20 | } 21 | } 22 | 23 | #pragma mark - 24 | 25 | @implementation UIView (ObjcSwizzle) 26 | 27 | + (void)load { 28 | static dispatch_once_t onceToken; 29 | dispatch_once(&onceToken, ^{ 30 | if (self.class == UIView.class) { 31 | EKSwizzleInstanceMethod(self.class, @selector(pointInside:withEvent:), @selector(_ek_pointInside:with:)); 32 | } 33 | }); 34 | } 35 | 36 | @end 37 | 38 | #pragma mark - 39 | 40 | @implementation UILabel (ObjcSwizzle) 41 | 42 | + (void)load { 43 | static dispatch_once_t onceToken; 44 | dispatch_once(&onceToken, ^{ 45 | if (self.class == UILabel.class) { 46 | EKSwizzleInstanceMethod(self.class, @selector(intrinsicContentSize), @selector(_ek_intrinsicContentSize)); 47 | EKSwizzleInstanceMethod(self.class, @selector(drawInRect:), @selector(_ek_drawTextIn:)); 48 | } 49 | }); 50 | } 51 | 52 | @end 53 | 54 | #pragma mark - 55 | 56 | @implementation UITextField (ObjcSwizzle) 57 | 58 | + (void)load { 59 | static dispatch_once_t onceToken; 60 | dispatch_once(&onceToken, ^{ 61 | if (self.class == UITextField.class) { 62 | EKSwizzleInstanceMethod(self.class, @selector(intrinsicContentSize), @selector(_ek_intrinsicContentSize)); 63 | EKSwizzleInstanceMethod(self.class, @selector(textRectForBounds:), @selector(_ek_textRectForBounds:)); 64 | EKSwizzleInstanceMethod(self.class, @selector(editingRectForBounds:), @selector(_ek_editingRectForBounds:)); 65 | } 66 | }); 67 | } 68 | 69 | @end 70 | -------------------------------------------------------------------------------- /Sources/Extension/UILabel+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UILabel+Extension.swift 3 | // Copyright (c) 2015-2016 Red Rain (http://mochxiao.com). 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | // THE SOFTWARE. 22 | // 23 | 24 | import UIKit 25 | 26 | private struct AssociationKey { 27 | fileprivate static var contentInsets: String = "com.mochxiao.uilabel.contentInsets" 28 | } 29 | 30 | public extension UILabel { 31 | public var contentInsets: UIEdgeInsets { 32 | get { 33 | if let value = associatedObject(forKey: &AssociationKey.contentInsets) as? NSValue { 34 | return value.uiEdgeInsetsValue 35 | } 36 | return UIEdgeInsets.zero 37 | } 38 | set { associate(retainObject: NSValue(uiEdgeInsets: newValue), forKey: &AssociationKey.contentInsets) } 39 | } 40 | 41 | var _ek_intrinsicContentSize: CGSize { 42 | // MARK: 4 fucking Xcode8/iOS10 SDKs 43 | setNeedsLayout() 44 | layoutIfNeeded() 45 | 46 | let size = sizeThatFits(CGSize(width: bounds.size.width, height: bounds.size.height)) 47 | let width = size.width + contentInsets.left + contentInsets.right 48 | let height = size.height + contentInsets.top + contentInsets.bottom 49 | return CGSize(width: width, height: height) 50 | } 51 | 52 | func _ek_drawText(in rect: CGRect) { 53 | _ek_drawText(in: UIEdgeInsetsInsetRect(rect, contentInsets)) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Sources/Extension/UITextView+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UITextView+Extension.swift 3 | // Copyright (c) 2015-2016 Red Rain (http://mochxiao.com). 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | // THE SOFTWARE. 22 | // 23 | 24 | import UIKit 25 | 26 | // MARK: - AssociationKey 27 | 28 | private struct AssociationKey { 29 | fileprivate static var textViewTextObserver: String = "com.mochxiao.uitextview.textViewTextObserver" 30 | } 31 | 32 | // MARK: - TextObserver Extension 33 | 34 | private extension UITextView { 35 | var textViewTextObserver: TextObserver { 36 | get { return associatedObject(forKey: &AssociationKey.textViewTextObserver) as! TextObserver } 37 | set { associate(retainObject: newValue, forKey: &AssociationKey.textViewTextObserver) } 38 | } 39 | } 40 | 41 | public extension UITextView { 42 | public func setupTextObserver(maxLength: Int = 100, actionHandler: ((Int) -> ())? = nil) { 43 | let textObserver = TextObserver(maxLength: maxLength) { (remainCount) -> () in 44 | actionHandler?(remainCount) 45 | } 46 | textObserver.observe(textView: self) 47 | textViewTextObserver = textObserver 48 | } 49 | } 50 | 51 | // MARK: - 52 | 53 | public extension UITextView { 54 | public func scrollCursorToVisible() { 55 | let cursorLocation = selectedRange.location 56 | let range = NSMakeRange(0, cursorLocation) 57 | scrollRangeToVisible(range) 58 | } 59 | } 60 | 61 | 62 | -------------------------------------------------------------------------------- /Sources/Extension/UIDevice+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIDevice+Extension.swift 3 | // Copyright (c) 2015-2016 Red Rain (http://mochxiao.com). 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | // THE SOFTWARE. 22 | // 23 | 24 | import UIKit 25 | 26 | public extension UIDevice { 27 | fileprivate static let _currentDevice = UIDevice.current 28 | 29 | public class var sysVersion: String { 30 | return _currentDevice.systemVersion 31 | } 32 | 33 | public class var majorVersion: Int { 34 | return Int(UIDevice.sysVersion.components(separatedBy: ".").first!)! 35 | } 36 | 37 | public class var iOS7x: Bool { 38 | return majorVersion >= 7 39 | } 40 | 41 | public class var iOS8x: Bool { 42 | return majorVersion >= 8 43 | } 44 | 45 | public class var iOS9x: Bool { 46 | return majorVersion >= 9 47 | } 48 | 49 | public class var iOS10x: Bool { 50 | return majorVersion >= 10 51 | } 52 | 53 | public class var iOS11x: Bool { 54 | return majorVersion >= 11 55 | } 56 | 57 | fileprivate static func deviceOrientation(_ result: (UIDeviceOrientation) -> ()) { 58 | if !_currentDevice.isGeneratingDeviceOrientationNotifications { 59 | _currentDevice.beginGeneratingDeviceOrientationNotifications() 60 | } 61 | result(_currentDevice.orientation) 62 | _currentDevice.endGeneratingDeviceOrientationNotifications() 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Sources/Extension/UIResponder+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIResponder+Extension.swift 3 | // Copyright (c) 2015-2016 Red Rain (http://mochxiao.com). 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | // THE SOFTWARE. 22 | // 23 | 24 | import UIKit 25 | 26 | public extension UIResponder { 27 | public func responder(ofClass cls: AnyClass) -> UIResponder? { 28 | var responder = self 29 | while let _responder = responder.next { 30 | if _responder.isKind(of: cls) { 31 | return _responder 32 | } 33 | 34 | responder = _responder 35 | } 36 | return nil 37 | } 38 | 39 | @discardableResult 40 | public func sendAction(_ action: Selector) -> Bool { 41 | return UIApplication.sendAction(action, fromSender: self) 42 | } 43 | 44 | @discardableResult 45 | public func performAction( 46 | _ action: Selector, 47 | _ firstArgument: AnyObject! = nil, 48 | _ secondArgument: AnyObject! = nil) -> Unmanaged! 49 | { 50 | var responder: UIResponder? = self 51 | while let _responder = responder , !_responder.responds(to: action) { 52 | responder = _responder.next 53 | } 54 | 55 | if nil == firstArgument { 56 | return responder?.perform(action) 57 | } else if nil == secondArgument { 58 | return responder?.perform(action, with: firstArgument) 59 | } else { 60 | return responder?.perform(action, with: firstArgument, with: secondArgument) 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Sources/Protocols/ExtensionProtocols.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExtensionProtocols.swift 3 | // Copyright (c) 2015-2016 Red Rain (http://mochxiao.com). 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | // THE SOFTWARE. 22 | // 23 | 24 | import Foundation 25 | 26 | // See: http://ericasadun.com/2016/04/18/default-reflection/ 27 | /// A reference type that provides a better default representation than the class name. 28 | public protocol DefaultReflectable: CustomStringConvertible {} 29 | 30 | /// A default implementation that enables class members to display their values. 31 | extension DefaultReflectable { 32 | /// Constructs a better representation using reflection. 33 | internal func defaultDescription(_ instance: T) -> String { 34 | let mirror = Mirror(reflecting: instance) 35 | let chunks = mirror.children.map { (label: String?, value: Any) -> String in 36 | if let label = label { 37 | if value is String { 38 | return "\(label): \"\(value)\"" 39 | } 40 | return "\(label): \(value)" 41 | } else { 42 | return "\(value)" 43 | } 44 | } 45 | if chunks.count > 0 { 46 | let chunksString = chunks.joined(separator: ", ") 47 | return "\(mirror.subjectType)(\(chunksString))" 48 | } else { 49 | return "\(instance)" 50 | } 51 | } 52 | 53 | /// Conforms to CustomStringConvertible. 54 | public var description: String { 55 | return defaultDescription(self) 56 | } 57 | } 58 | // 59 | //extension NSObject: DefaultReflectable { 60 | //} 61 | 62 | -------------------------------------------------------------------------------- /Sources/Classes/DashlineView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DashlineView.swift 3 | // Copyright (c) 2015-2016 Red Rain (http://mochxiao.com). 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | // THE SOFTWARE. 22 | // 23 | 24 | import UIKit 25 | 26 | final public class DashlineView: UIView { 27 | public var spacing: CGFloat = 2 28 | public var lineColor: UIColor = UIColor.separator 29 | public var horizontal: Bool = true 30 | 31 | public override func draw(_ rect: CGRect) { 32 | super.draw(rect) 33 | 34 | backgroundColor?.setFill() 35 | UIRectFill(rect) 36 | 37 | let lineWidth = horizontal ? rect.height.ceilling : rect.width.ceilling 38 | let startPoint = horizontal ? CGPoint(x: (lineWidth / 2).ceilling, y: (rect.height / 2).ceilling) : 39 | CGPoint(x: (rect.width / 2).ceilling , y: (lineWidth / 2).ceilling) 40 | let endPoint = horizontal ? CGPoint(x: rect.width - (lineWidth / 2).ceilling, y: (rect.height / 2).ceilling) : 41 | CGPoint(x: (rect.width / 2).ceilling , y: rect.height - (lineWidth / 2).ceilling) 42 | 43 | guard let context = UIGraphicsGetCurrentContext() else { 44 | return 45 | } 46 | context.beginPath() 47 | context.setLineWidth(lineWidth) 48 | context.setStrokeColor(lineColor.cgColor) 49 | context.setLineDash(phase: 0, lengths: [spacing, spacing]) 50 | context.move(to: CGPoint(x: startPoint.x, y: startPoint.y)) 51 | context.addLine(to: CGPoint(x: endPoint.x, y: endPoint.y)) 52 | context.strokePath() 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Sources/Extension/UserDefaults+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserDefaults+Extension.swift 3 | // Copyright (c) 2015-2016 Red Rain (http://mochxiao.com). 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | // THE SOFTWARE. 22 | // 23 | 24 | import UIKit 25 | 26 | public extension Foundation.UserDefaults { 27 | public subscript(key: String) -> Any? { 28 | get { return value(forKey: key) as Any } 29 | set { 30 | switch newValue { 31 | case let value as Int: set(value, forKey: key) 32 | case let value as Double: set(value, forKey: key) 33 | case let value as Bool: set(value, forKey: key) 34 | case let value as URL: set(value, forKey: key) 35 | case let value as NSObject: set(value, forKey: key) 36 | case nil: removeObject(forKey: key) 37 | default: assertionFailure("Invalid value type.") 38 | } 39 | } 40 | } 41 | 42 | fileprivate func setter(key: String, value: Any?) { 43 | self[key] = value 44 | synchronize() 45 | } 46 | 47 | /// Is there a object for specific key exist. 48 | public func hasKey(_ key: String) -> Bool { 49 | return nil != object(forKey: key) 50 | } 51 | 52 | /// Archive object to NSData to save. 53 | public func archive(object: Any?, forKey key: String) { 54 | if let value = object { 55 | setter(key: key, value: NSKeyedArchiver.archivedData(withRootObject: value) as Any?) 56 | } else { 57 | removeObject(forKey: key) 58 | } 59 | } 60 | 61 | /// Unarchive object for specific key. 62 | public func unarchivedObject(forKey key: String) -> Any? { 63 | return data(forKey: key).flatMap { NSKeyedUnarchiver.unarchiveObject(with: $0) as Any } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Sources/Classes/CustomHeightNavigationBar.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomHeightNavigationBar.swift 3 | // Copyright (c) 2015-2016 Red Rain (http://mochxiao.com). 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | // THE SOFTWARE. 22 | // 23 | 24 | import UIKit 25 | 26 | // See: http://www.emdentec.com/blog/2014/2/25/hacking-uinavigationbar 27 | open class CustomHeightNavigationBar: UINavigationBar { 28 | fileprivate let defaultHeight: CGFloat = 44 29 | @IBInspectable 30 | open var customHeight: CGFloat = 88 31 | 32 | public override init(frame: CGRect) { 33 | super.init(frame: frame) 34 | commitInit() 35 | } 36 | 37 | public required init?(coder aDecoder: NSCoder) { 38 | super.init(coder: aDecoder) 39 | commitInit() 40 | } 41 | 42 | open override func awakeFromNib() { 43 | super.awakeFromNib() 44 | commitInit() 45 | } 46 | 47 | fileprivate func commitInit() { 48 | transform = CGAffineTransform.identity 49 | transform = CGAffineTransform(translationX: 0, y: defaultHeight - customHeight) 50 | } 51 | 52 | open override func sizeThatFits(_ size: CGSize) -> CGSize { 53 | var newSize = super.sizeThatFits(size) 54 | newSize.height = customHeight 55 | return newSize 56 | } 57 | 58 | open override func layoutSubviews() { 59 | super.layoutSubviews() 60 | 61 | let classNamesToReposition = ["_UINavigationBarBackground"] 62 | for view in subviews { 63 | if classNamesToReposition.contains(NSStringFromClass(type(of: view))) { 64 | var newFrame = view.frame 65 | let statusBarHeight = UIApplication.shared.statusBarFrame.size.height 66 | newFrame.origin.y = bounds.origin.y + customHeight - defaultHeight - statusBarHeight 67 | newFrame.size.height = bounds.size.height + statusBarHeight 68 | view.frame = newFrame 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Sources/Extension/NSObject+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSObject+Extension.swift 3 | // Copyright (c) 2015-2016 Red Rain (http://mochxiao.com). 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | // THE SOFTWARE. 22 | // 23 | 24 | import Foundation 25 | 26 | // MARK: - Associate Object 27 | 28 | public extension NSObject { 29 | /// Sets an associated value for a given object using a weak reference to the associated object. 30 | /// **Note**: the `key` underlying type must be String. 31 | public func associate(assignObject object: Any?, forKey key: UnsafeRawPointer) { 32 | let strKey: String = convertUnsafePointerToSwiftType(key) 33 | willChangeValue(forKey: strKey) 34 | objc_setAssociatedObject(self, key, object, .OBJC_ASSOCIATION_ASSIGN) 35 | didChangeValue(forKey: strKey) 36 | } 37 | 38 | /// Sets an associated value for a given object using a strong reference to the associated object. 39 | /// **Note**: the `key` underlying type must be String. 40 | public func associate(retainObject object: Any?, forKey key: UnsafeRawPointer) { 41 | let strKey: String = convertUnsafePointerToSwiftType(key) 42 | willChangeValue(forKey: strKey) 43 | objc_setAssociatedObject(self, key, object, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 44 | didChangeValue(forKey: strKey) 45 | } 46 | 47 | /// Sets an associated value for a given object using a copied reference to the associated object. 48 | /// **Note**: the `key` underlying type must be String. 49 | public func associate(copyObject object: Any?, forKey key: UnsafeRawPointer) { 50 | let strKey: String = convertUnsafePointerToSwiftType(key) 51 | willChangeValue(forKey: strKey) 52 | objc_setAssociatedObject(self, key, object, .OBJC_ASSOCIATION_COPY_NONATOMIC) 53 | didChangeValue(forKey: strKey) 54 | } 55 | 56 | /// Returns the value associated with a given object for a given key. 57 | /// **Note**: the `key` underlying type must be String. 58 | public func associatedObject(forKey key: UnsafeRawPointer) -> Any? { 59 | return objc_getAssociatedObject(self, key) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /ExtensionKit.xcodeproj/xcshareddata/xcschemes/ExtensionKit.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 33 | 34 | 35 | 36 | 47 | 48 | 54 | 55 | 56 | 57 | 58 | 59 | 65 | 66 | 72 | 73 | 74 | 75 | 77 | 78 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /Sources/Extension/UITextField+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UITextField+Extension.swift 3 | // Copyright (c) 2015-2016 Red Rain (http://mochxiao.com). 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | // THE SOFTWARE. 22 | // 23 | 24 | import UIKit 25 | 26 | // MARK: - AssociationKey 27 | 28 | private struct AssociationKey { 29 | fileprivate static var textFieldTextObserver: String = "com.mochxiao.uitextfield.textFieldTextObserver" 30 | fileprivate static var contentInsets: String = "com.mochxiao.uitextfield.contentInsets" 31 | } 32 | 33 | // MARK: - 34 | 35 | public extension UITextField { 36 | public var contentInsets: UIEdgeInsets { 37 | get { 38 | if let value = associatedObject(forKey: &AssociationKey.contentInsets) as? NSValue { 39 | return value.uiEdgeInsetsValue 40 | } 41 | return UIEdgeInsets.zero 42 | } 43 | set { associate(retainObject: NSValue(uiEdgeInsets: newValue), forKey: &AssociationKey.contentInsets) } 44 | } 45 | 46 | var _ek_intrinsicContentSize: CGSize { 47 | // MARK: 4 fucking Xcode8/iOS10 SDKs 48 | setNeedsLayout() 49 | layoutIfNeeded() 50 | 51 | let size = sizeThatFits(CGSize(width: bounds.size.width, height: bounds.size.height)) 52 | let width = size.width + contentInsets.left + contentInsets.right 53 | let height = size.height + contentInsets.top + contentInsets.bottom 54 | return CGSize(width: width, height: height) 55 | } 56 | 57 | func _ek_textRect(forBounds bounds: CGRect) -> CGRect { 58 | return UIEdgeInsetsInsetRect(bounds, contentInsets) 59 | } 60 | 61 | func _ek_editingRect(forBounds bounds: CGRect) -> CGRect { 62 | return UIEdgeInsetsInsetRect(bounds, contentInsets) 63 | } 64 | } 65 | 66 | // MARK: - TextObserver Extension 67 | 68 | private extension UITextField { 69 | var textFieldTextObserver: TextObserver { 70 | get { return associatedObject(forKey: &AssociationKey.textFieldTextObserver) as! TextObserver } 71 | set { associate(retainObject: newValue, forKey: &AssociationKey.textFieldTextObserver) } 72 | } 73 | } 74 | 75 | public extension UITextField { 76 | public func setupTextObserver(maxLength: Int = 100, actionHandler: ((Int) -> ())? = nil) { 77 | let textObserver = TextObserver(maxLength: maxLength) { (remainCount) -> () in 78 | actionHandler?(remainCount) 79 | } 80 | textObserver.observe(textField: self) 81 | textFieldTextObserver = textObserver 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Sources/Extension/Dictionary+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Dictionary+Extension.swift 3 | // Copyright (c) 2015-2016 Red Rain (http://mochxiao.com). 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | // THE SOFTWARE. 22 | // 23 | 24 | import Foundation 25 | 26 | // MARK: - Dictionary 27 | 28 | public extension Dictionary { 29 | private func queryComponents(fromKey key: String, value: Any) -> [(String, String)] { 30 | var components: [(String, String)] = [] 31 | 32 | if let dictionary = value as? [String: Any] { 33 | for (nestedKey, value) in dictionary { 34 | components += queryComponents(fromKey: "\(key)[\(nestedKey)]", value: value) 35 | } 36 | } else if let array = value as? [Any] { 37 | for value in array { 38 | components += queryComponents(fromKey: "\(key)[]", value: value) 39 | } 40 | } else if let value = value as? NSNumber { 41 | if value.isBool { 42 | components.append((key.escaped, (value.boolValue ? "1" : "0").escaped)) 43 | } else { 44 | components.append((key.escaped, "\(value)".escaped)) 45 | } 46 | } else if let bool = value as? Bool { 47 | components.append((key.escaped, (bool ? "1" : "0").escaped)) 48 | } else { 49 | components.append((key.escaped, "\(value)".escaped)) 50 | } 51 | 52 | return components 53 | } 54 | 55 | /// Stolen from Alamofire 56 | private func query(_ parameters: [String: Any]) -> String { 57 | var components: [(String, String)] = [] 58 | 59 | for key in parameters.keys.sorted(by: <) { 60 | let value = parameters[key]! 61 | components += queryComponents(fromKey: key, value: value) 62 | } 63 | 64 | return components.map { "\($0)=\($1)" }.joined(separator: "&") 65 | } 66 | 67 | public var queryString: String { 68 | var newDic: [String: Any] = [:] 69 | for element in self { 70 | newDic["\(element.key)"] = element.value 71 | } 72 | return query(newDic) 73 | } 74 | 75 | public var JSONString: String? { 76 | if let data = Data.make(fromJSONObject: self as AnyObject) { 77 | return String(data: data, encoding: String.Encoding.utf8) 78 | } 79 | return nil 80 | } 81 | } 82 | 83 | /// Combine two `Dictionary` to one. 84 | public func += ( 85 | lhs: inout Dictionary, 86 | rhs: Dictionary) 87 | { 88 | for (key, value) in rhs { 89 | lhs.updateValue(value, forKey: key) 90 | } 91 | } 92 | 93 | -------------------------------------------------------------------------------- /Sources/Extension/UIBarButtonItem+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIBarButtonItem+Extension.swift 3 | // Copyright (c) 2015-2016 Red Rain (http://mochxiao.com). 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | // THE SOFTWARE. 22 | // 23 | 24 | import UIKit 25 | 26 | // MARK: - AssociationKey 27 | 28 | private struct AssociationKey { 29 | fileprivate static var barButtonItemActionHandlerWrapper: String = "com.mochxiao.uibarbuttonitem.barButtonItemActionHandlerWrapper" 30 | } 31 | 32 | // MARK: - 33 | 34 | public extension UIBarButtonItem { 35 | public class func make(fixedSpace width: CGFloat) -> UIBarButtonItem { 36 | let spacing = UIBarButtonItem(barButtonSystemItem: .fixedSpace, target: nil, action: nil) 37 | spacing.width = width 38 | return spacing 39 | } 40 | } 41 | 42 | // MARK: - 43 | 44 | public extension UIBarButtonItem { 45 | fileprivate var barButtonItemActionHandlerWrapper: ClosureDecorator? { 46 | get { return associatedObject(forKey: &AssociationKey.barButtonItemActionHandlerWrapper) as? ClosureDecorator } 47 | set { associate(retainObject: newValue, forKey: &AssociationKey.barButtonItemActionHandlerWrapper) } 48 | } 49 | 50 | public class func make(title: String, actionHandler: ((UIBarButtonItem) -> ())?) -> UIBarButtonItem { 51 | let barButtonItem = UIBarButtonItem(title: title, style: .plain, target: self, action: #selector(UIBarButtonItem.performActionHandler(sender:))) 52 | if let actionHandler = actionHandler { 53 | barButtonItem.barButtonItemActionHandlerWrapper = ClosureDecorator(actionHandler) 54 | } 55 | return barButtonItem 56 | } 57 | 58 | public class func make(image: UIImage?, actionHandler: ((UIBarButtonItem) -> ())?) -> UIBarButtonItem { 59 | let barButtonItem = UIBarButtonItem(image: image?.original, style: .plain, target: self, action: #selector(UIBarButtonItem.performActionHandler(sender:))) 60 | if let actionHandler = actionHandler { 61 | barButtonItem.barButtonItemActionHandlerWrapper = ClosureDecorator(actionHandler) 62 | } 63 | return barButtonItem 64 | } 65 | 66 | public class func make(systemItem item: UIBarButtonSystemItem, actionHandler: ((UIBarButtonItem) -> ())?) -> UIBarButtonItem { 67 | let barButtonItem = UIBarButtonItem(barButtonSystemItem: item, target: self, action: #selector(UIBarButtonItem.performActionHandler(sender:))) 68 | if let actionHandler = actionHandler { 69 | barButtonItem.barButtonItemActionHandlerWrapper = ClosureDecorator(actionHandler) 70 | } 71 | return barButtonItem 72 | } 73 | 74 | /// Helper func 75 | @objc internal class func performActionHandler(sender: UIBarButtonItem) { 76 | sender.barButtonItemActionHandlerWrapper?.invoke(sender) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Sources/Extension/UIApplication+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIApplication+Extension.swift 3 | // Copyright (c) 2015-2016 Red Rain (http://mochxiao.com). 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | // THE SOFTWARE. 22 | // 23 | 24 | import UIKit 25 | 26 | // MARK: - Actions 27 | 28 | public extension UIApplication { 29 | fileprivate static let _sharedApplication = UIApplication.shared 30 | 31 | public class func open(url: Foundation.URL) { 32 | if _sharedApplication.canOpenURL(url) { 33 | _sharedApplication.openURL(url) 34 | } else { 35 | logging("Can not execute the given action.") 36 | } 37 | } 38 | 39 | public class func open(urlPath: String) { 40 | if let url = URL(string: urlPath) { 41 | UIApplication.open(url: url) 42 | } 43 | } 44 | 45 | public class func makePhone(to phoneNumber: String) { 46 | open(urlPath: "telprompt:\(phoneNumber)") 47 | } 48 | 49 | public class func sendMessage(to phoneNumber: String) { 50 | open(urlPath: "sms:\(phoneNumber)") 51 | } 52 | 53 | public class func email(to email: String) { 54 | open(urlPath: "mailto:\(email)") 55 | } 56 | 57 | public class func chatQQ(to qq: String) { 58 | open(urlPath: "mqq://im/chat?chat_type=wpa&uin=\(qq)&version=1&src_type=iOS") 59 | } 60 | 61 | public class func clearIconBadge() { 62 | let badgeNumber = _sharedApplication.applicationIconBadgeNumber 63 | _sharedApplication.applicationIconBadgeNumber = 1 64 | _sharedApplication.applicationIconBadgeNumber = 0 65 | _sharedApplication.cancelAllLocalNotifications() 66 | _sharedApplication.applicationIconBadgeNumber = badgeNumber 67 | } 68 | 69 | public class func sendAction(_ action: Selector, fromSender sender: AnyObject?, forEvent event: UIEvent? = nil) -> Bool { 70 | // Get the target in the responder chain 71 | var target = sender 72 | 73 | while let _target = target , !_target.canPerformAction(action, withSender: sender) { 74 | target = _target.next 75 | } 76 | 77 | if let _target = target { 78 | return UIApplication.shared.sendAction(action, to: _target, from: sender, for: event) 79 | } 80 | 81 | return false 82 | } 83 | 84 | /// Setting the statusBarStyle does nothing if your application is using the default UIViewController-based status bar system. 85 | public class func makeStatusBarDark() { 86 | UIApplication.shared.statusBarStyle = .default 87 | } 88 | 89 | /// Setting the statusBarStyle does nothing if your application is using the default UIViewController-based status bar system. 90 | public class func makeStatusBarLight() { 91 | UIApplication.shared.statusBarStyle = .lightContent 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Sources/Classes/TextObserver.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TextObserver.swift 3 | // Copyright (c) 2015-2016 Red Rain (http://mochxiao.com). 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | // THE SOFTWARE. 22 | // 23 | 24 | import UIKit 25 | 26 | // MARK: - TextObserver 27 | 28 | final public class TextObserver { 29 | fileprivate let maxLength: Int 30 | fileprivate let actionHandler: ((Int) -> ()) 31 | fileprivate var textFieldObserver: NSObjectProtocol? 32 | fileprivate var textViewObserver: NSObjectProtocol? 33 | 34 | public init(maxLength: Int, actionHandler: @escaping ((Int) -> ())) { 35 | self.maxLength = maxLength 36 | self.actionHandler = actionHandler 37 | } 38 | 39 | deinit { 40 | if let textFieldObserver = textFieldObserver { 41 | NotificationCenter.default.removeObserver(textFieldObserver) 42 | } 43 | 44 | if let textViewObserver = textViewObserver { 45 | NotificationCenter.default.removeObserver(textViewObserver) 46 | } 47 | } 48 | 49 | // MARK: - UITextField 50 | 51 | public func observe(textField object: UITextField) { 52 | textFieldObserver = NotificationCenter.default.addObserver( 53 | forName: NSNotification.Name.UITextFieldTextDidChange, 54 | object: object, 55 | queue: OperationQueue.main) { [weak self] (notification) -> Void in 56 | guard let this = self else { return } 57 | guard let textField = notification.object as? UITextField else { return } 58 | guard let text = textField.text else { return } 59 | 60 | let textLenght = text.count 61 | if textLenght > this.maxLength && nil == textField.markedTextRange { 62 | textField.text = text.substring(toIndex: this.maxLength) 63 | } 64 | 65 | this.actionHandler(this.maxLength - (textField.text ?? "").count) 66 | } 67 | } 68 | 69 | // MARK: - UITextView 70 | 71 | public func observe(textView object: UITextView) { 72 | textViewObserver = NotificationCenter.default.addObserver( 73 | forName: NSNotification.Name.UITextViewTextDidChange, 74 | object: object, 75 | queue: OperationQueue.main) { [weak self] (notification) -> Void in 76 | guard let this = self else { return } 77 | guard let textView = notification.object as? UITextView else { return } 78 | guard let text = textView.text else { return } 79 | 80 | let textLenght = text.count 81 | if textLenght > this.maxLength && nil == textView.markedTextRange { 82 | textView.text = text.substring(toIndex: this.maxLength) 83 | } 84 | 85 | this.actionHandler(this.maxLength - (textView.text ?? "").count) 86 | textView.scrollCursorToVisible() 87 | } 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /Sources/Extension/Array+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Array+Extension.swift 3 | // Copyright (c) 2015-2016 Red Rain (http://mochxiao.com). 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | // THE SOFTWARE. 22 | // 23 | 24 | import Foundation 25 | 26 | // MARK: - Array 27 | 28 | public extension Array { 29 | public mutating func exchange(lhs index: Int, rhs otherIndex: Int) { 30 | if count <= index || count <= otherIndex { 31 | fatalError("Index beyond boundary.") 32 | } 33 | if index >= otherIndex { 34 | fatalError("lhs must less than rhs.") 35 | } 36 | 37 | let firstItemData = self[index] 38 | let firstRange = Range(index ..< index + 1) 39 | 40 | let secondaryItemData = self[otherIndex] 41 | let secondaryRange = Range(otherIndex ..< otherIndex + 1) 42 | 43 | replaceSubrange(firstRange, with: [secondaryItemData]) 44 | replaceSubrange(secondaryRange, with: [firstItemData]) 45 | } 46 | 47 | public mutating func replace(at index: Int, with element: Element) { 48 | if count <= index { 49 | fatalError("Index beyond boundary.") 50 | } 51 | let range = Range(index ..< index + 1) 52 | replaceSubrange(range, with: [element]) 53 | } 54 | 55 | public mutating func replaceLast(_ element: Element) { 56 | replace(at: count - 1, with: element) 57 | } 58 | 59 | public mutating func replaceFirst(_ element: Element) { 60 | replace(at: 0, with: element) 61 | } 62 | 63 | public var prettyDebugDescription: String { 64 | var output: [String] = [] 65 | var index = 0 66 | for item in self { 67 | output.append("\(index): \(item)") 68 | index += 1 69 | } 70 | return output.joined(separator: "\n") 71 | } 72 | 73 | public var second: Element? { 74 | if count > 1 { return self[1] } 75 | return nil 76 | } 77 | 78 | public var third: Element? { 79 | if count > 2 { return self[2] } 80 | return nil 81 | } 82 | 83 | public var fourth: Element? { 84 | if count > 3 { return self[3] } 85 | return nil 86 | } 87 | 88 | public var fifthly: Element? { 89 | if count > 4 { return self[4] } 90 | return nil 91 | } 92 | 93 | public var sixth: Element? { 94 | if count > 5 { return self[5] } 95 | return nil 96 | } 97 | 98 | public var seventh: Element? { 99 | if count > 6 { return self[6] } 100 | return nil 101 | } 102 | 103 | public var eighth: Element? { 104 | if count > 7 { return self[7] } 105 | return nil 106 | } 107 | 108 | public var ninth: Element? { 109 | if count > 8 { return self[8] } 110 | return nil 111 | } 112 | 113 | public var tenth: Element? { 114 | if count > 9 { return self[9] } 115 | return nil 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /Sources/Extension/Number+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Number+Extension.swift 3 | // Copyright (c) 2015-2016 Red Rain (http://mochxiao.com). 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | // THE SOFTWARE. 22 | // 23 | 24 | import Foundation 25 | 26 | public extension NSNumber { 27 | public var isBool: Bool { return CFBooleanGetTypeID() == CFGetTypeID(self) } 28 | } 29 | 30 | // MARK: - Double -> String 31 | 32 | public extension Double { 33 | public var price: String { 34 | let str = String(format: "%.2f", self) 35 | if str.hasSuffix("00") { 36 | return String(format: "%.0f", self) 37 | } 38 | if str.hasSuffix("0") { 39 | return String(format: "%.1f", self) 40 | } 41 | return str 42 | } 43 | public var CNY: String { 44 | return "¥" + price 45 | } 46 | 47 | public var USD: String { 48 | return "$" + price 49 | } 50 | 51 | public var string: String { 52 | return String(self) 53 | } 54 | 55 | public var cgfloat: CGFloat { 56 | return CGFloat(self) 57 | } 58 | } 59 | 60 | // MARK: - Time & Date 61 | 62 | public extension Double { 63 | var formattedDate: String { 64 | let formatter = DateFormatter() 65 | formatter.dateFormat = "yyyy-MM-dd" 66 | let date = Date(timeIntervalSince1970: self) 67 | return formatter.string(from: date) 68 | } 69 | 70 | var formattedTime: String { 71 | let formatter = DateFormatter() 72 | formatter.dateFormat = "HH:mm" 73 | let date = Date(timeIntervalSince1970: self) 74 | return formatter.string(from: date) 75 | } 76 | 77 | var formattedDateTime: String { 78 | let formatter = DateFormatter() 79 | formatter.dateFormat = "yyyy-MM-dd HH:mm" 80 | let date = Date(timeIntervalSince1970: self) 81 | return formatter.string(from: date) 82 | } 83 | } 84 | 85 | public extension Int { 86 | var formattedDate: String { 87 | return Double(self).formattedDate 88 | } 89 | 90 | var formattedTime: String { 91 | return Double(self).formattedTime 92 | } 93 | 94 | var formattedDateTime: String { 95 | return Double(self).formattedDateTime 96 | } 97 | 98 | public var cgfloat: CGFloat { 99 | return CGFloat(self) 100 | } 101 | } 102 | 103 | public extension UInt { 104 | public var cgfloat: CGFloat { 105 | return CGFloat(self) 106 | } 107 | } 108 | 109 | public extension UInt8 { 110 | public var cgfloat: CGFloat { 111 | return CGFloat(self) 112 | } 113 | } 114 | 115 | public extension UInt16 { 116 | public var cgfloat: CGFloat { 117 | return CGFloat(self) 118 | } 119 | } 120 | 121 | public extension UInt32 { 122 | public var cgfloat: CGFloat { 123 | return CGFloat(self) 124 | } 125 | } 126 | 127 | public extension UInt64 { 128 | public var cgfloat: CGFloat { 129 | return CGFloat(self) 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /Sources/Classes/PlaceholderTextView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlaceholderTextView.swift 3 | // Copyright (c) 2015-2016 Red Rain (http://mochxiao.com). 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | // THE SOFTWARE. 22 | // 23 | 24 | import UIKit 25 | 26 | /// The UITextView subclass supported a placeholder property like UITextField. 27 | /// Optional has a counting remain text length present label. 28 | /// **Note**: Do not forget invoke `invokeTextObserver(maxLength:, actionHandler:)`. 29 | final public class PlaceholderTextView: UITextView { 30 | fileprivate let placeholderLabel: UILabel = { 31 | let label = UILabel() 32 | label.backgroundColor = UIColor.clear 33 | label.textColor = UIColor.placeholder 34 | label.font = UIFont.preferredFont(forTextStyle: UIFontTextStyle.footnote) 35 | label.lineBreakMode = .byTruncatingTail 36 | label.numberOfLines = 0 37 | return label 38 | }() 39 | 40 | public var placeholder: String? { 41 | didSet { 42 | placeholderLabel.text = placeholder 43 | updatePlaceholderLabelFrame() 44 | } 45 | } 46 | 47 | override public var font: UIFont! { 48 | didSet { 49 | placeholderLabel.font = font 50 | updatePlaceholderLabelFrame() 51 | } 52 | } 53 | 54 | override public var text: String? { 55 | didSet { 56 | if let text = text , text.count > 0 { 57 | placeholderLabel.isHidden = true 58 | } else { 59 | placeholderLabel.isHidden = false 60 | } 61 | } 62 | } 63 | 64 | public override init(frame: CGRect, textContainer: NSTextContainer?) { 65 | super.init(frame: frame, textContainer: textContainer) 66 | setup() 67 | } 68 | 69 | required public init?(coder aDecoder: NSCoder) { 70 | super.init(coder: aDecoder) 71 | setup() 72 | } 73 | 74 | public override func layoutSubviews() { 75 | super.layoutSubviews() 76 | updatePlaceholderLabelFrame() 77 | } 78 | } 79 | 80 | public extension PlaceholderTextView { 81 | fileprivate func setup() { 82 | addSubview(placeholderLabel) 83 | } 84 | 85 | fileprivate func updatePlaceholderLabelFrame() { 86 | if let size = placeholderLabel.text?.layoutSize( 87 | font: placeholderLabel.font, 88 | preferredMaxLayoutWidth: bounds.width - 10) 89 | { 90 | placeholderLabel.frame = CGRect( 91 | x: 5, 92 | y: 4, 93 | width: size.width + 10, 94 | height: size.height + 8 95 | ) 96 | } 97 | } 98 | 99 | /// **Note**: Do not invoke `setupTextObserver(maxLength:, actionHandler:)` the both. 100 | public func invokeTextObserver(maxLength: Int = 100, actionHandler: ((Int) -> ())? = nil) { 101 | setupTextObserver(maxLength: maxLength) { [weak self] (remainCount) -> () in 102 | self?.placeholderLabel.isHidden = remainCount != maxLength 103 | actionHandler?(remainCount) 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /Sources/Extension/CALayer+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CALayer+Extension.swift 3 | // ExtensionKit 4 | // 5 | // Created by Moch Xiao on 2/22/17. 6 | // Copyright © 2017 Moch. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import QuartzCore 11 | 12 | // MARK: - Frame & Struct 13 | public extension CALayer { 14 | public var origin: CGPoint { 15 | get { return frame.origin } 16 | set { frame = CGRect(x: newValue.x, y: newValue.y, width: width, height: height) } 17 | } 18 | 19 | public var size: CGSize { 20 | get { return frame.size } 21 | set { frame = CGRect(x: minX, y: minY, width: newValue.width, height: newValue.height) } 22 | } 23 | 24 | public var minX: CGFloat { 25 | get { return frame.origin.x } 26 | set { frame = CGRect(x: newValue, y: minY, width: width, height: height) } 27 | } 28 | 29 | public var left: CGFloat { 30 | get { return frame.origin.x } 31 | set { frame = CGRect(x: newValue, y: minY, width: width, height: height) } 32 | } 33 | 34 | public var midX: CGFloat { 35 | get { return frame.midX } 36 | set { frame = CGRect(x: newValue - width * 0.5, y: minY, width: width, height: height) } 37 | } 38 | 39 | public var centerX: CGFloat { 40 | get { return frame.midX } 41 | set { frame = CGRect(x: newValue - width * 0.5, y: minY, width: width, height: height) } 42 | } 43 | 44 | public var maxX: CGFloat { 45 | get { return minX + width } 46 | set { frame = CGRect(x: newValue - width, y: minY, width: width, height: height) } 47 | } 48 | 49 | public var right: CGFloat { 50 | get { return minX + width } 51 | set { frame = CGRect(x: newValue - width, y: minY, width: width, height: height) } 52 | } 53 | 54 | public var minY: CGFloat { 55 | get { return frame.origin.y } 56 | set { frame = CGRect(x: minX, y: newValue, width: width, height: height) } 57 | } 58 | 59 | public var top: CGFloat { 60 | get { return frame.origin.y } 61 | set { frame = CGRect(x: minX, y: newValue, width: width, height: height) } 62 | } 63 | 64 | public var midY: CGFloat { 65 | get { return frame.midY } 66 | set { frame = CGRect(x: minX, y: newValue - height * 0.5, width: width, height: height) } 67 | } 68 | 69 | public var centerY: CGFloat { 70 | get { return frame.midY } 71 | set { frame = CGRect(x: minX, y: newValue - height * 0.5, width: width, height: height) } 72 | } 73 | 74 | public var maxY: CGFloat { 75 | get { return minY + height } 76 | set { frame = CGRect(x: minX, y: newValue - height, width: width, height: height) } 77 | } 78 | 79 | public var bottom: CGFloat { 80 | get { return minY + height } 81 | set { frame = CGRect(x: minX, y: newValue - height, width: width, height: height) } 82 | } 83 | 84 | public var width: CGFloat { 85 | get { return bounds.width } 86 | set { frame = CGRect(x: minX, y: minY, width: newValue, height: height) } 87 | } 88 | 89 | public var height: CGFloat { 90 | get { return bounds.height } 91 | set { frame = CGRect(x: minX, y: minY, width: width, height: newValue) } 92 | } 93 | 94 | public var center: CGPoint { 95 | set { frame = CGRect(x: newValue.x - frame.size.width * 0.5, y: newValue.y - frame.size.height * 0.5, width: width, height: height) } 96 | get { return CGPoint(x: origin.x + size.width * 0.5, y: origin.y + size.height * 0.5) } 97 | } 98 | } 99 | 100 | public extension CALayer { 101 | public var snapshotImage: UIImage? { 102 | UIGraphicsBeginImageContextWithOptions(bounds.size, isOpaque, 0) 103 | guard let context = UIGraphicsGetCurrentContext() else { return nil } 104 | render(in: context) 105 | let image = UIGraphicsGetImageFromCurrentImageContext() 106 | UIGraphicsEndImageContext() 107 | return image 108 | } 109 | 110 | public func setShadow(color: UIColor, offset: CGSize, radius: CGFloat) { 111 | shadowColor = color.cgColor 112 | shadowOffset = offset 113 | shadowRadius = radius 114 | shadowOpacity = 1 115 | shouldRasterize = true 116 | rasterizationScale = UIScreen.main.scale 117 | } 118 | 119 | public func removeAllSublayers() { 120 | sublayers?.forEach { (sender) in 121 | sender.removeFromSuperlayer() 122 | } 123 | } 124 | } 125 | 126 | 127 | 128 | -------------------------------------------------------------------------------- /Sources/Classes/CGD.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGD.swift 3 | // Copyright (c) 2015-2016 Red Rain (http://mochxiao.com). 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | // THE SOFTWARE. 22 | // 23 | 24 | import Foundation 25 | 26 | // MARK: - Delay Task & Cancel 27 | 28 | public typealias Task = ((_ cancel: Bool) -> ()) 29 | 30 | @discardableResult 31 | public func delay(interval time: TimeInterval, task: @escaping (() -> ())) -> Task? { 32 | func dispatch_later(_ block: @escaping () -> ()) { 33 | DispatchQueue.main.asyncAfter( 34 | deadline: .now() + time, 35 | execute: block 36 | ) 37 | } 38 | 39 | var closure: (() -> ())? = task 40 | var result: Task? 41 | 42 | let delayedClosure: Task = { 43 | if let internalClosure = closure, !$0 { 44 | DispatchQueue.main.async(execute: internalClosure) 45 | } 46 | 47 | closure = nil 48 | result = nil 49 | } 50 | 51 | result = delayedClosure 52 | 53 | dispatch_later { 54 | if let delayedClosure = result { 55 | delayedClosure(false) 56 | } 57 | } 58 | 59 | return result 60 | } 61 | 62 | public func cancel(_ task: Task?) { 63 | task?(true) 64 | } 65 | 66 | // MARK: - Async Task 67 | 68 | public func mainThreadAsync(execute work: @escaping () -> ()) { 69 | if Thread.isMainThread { 70 | work() 71 | } else { 72 | DispatchQueue.main.async(execute: work) 73 | } 74 | } 75 | 76 | public func globalThreadAsync(execute work: @escaping () -> ()) { 77 | if !Thread.isMainThread { 78 | work() 79 | } else { 80 | DispatchQueue.global().async(execute: work) 81 | } 82 | } 83 | 84 | // MARK: - AsyncSerialWorker 85 | 86 | open class AsyncSerialWorker { 87 | private let serialQueue = DispatchQueue(label: "com.mochxiao.queue.serial") 88 | 89 | open func enqueue(work: @escaping (@escaping () -> ()) -> ()) { 90 | serialQueue.async { 91 | let semaphore = DispatchSemaphore(value: 0) 92 | work { semaphore.signal() } 93 | semaphore.wait() 94 | } 95 | } 96 | } 97 | 98 | // MARK: - LimitedWorker 99 | 100 | open class LimitedWorker { 101 | fileprivate let concurrentQueue = DispatchQueue(label: "com.mochxiao.queue.concurrent", attributes: .concurrent) 102 | fileprivate let semaphore: DispatchSemaphore 103 | 104 | public init(limit: Int) { 105 | semaphore = DispatchSemaphore(value: limit) 106 | } 107 | 108 | open func enqueue(work: @escaping () -> ()) { 109 | concurrentQueue.async { 110 | self.semaphore.wait() 111 | work() 112 | self.semaphore.signal() 113 | } 114 | } 115 | } 116 | 117 | // MARK: - IdentityMap 118 | 119 | public protocol Identifiable { 120 | var identifier: String { get } 121 | } 122 | 123 | extension Identifiable { 124 | var identifier: String { return UUID().uuidString } 125 | } 126 | 127 | extension NSObject: Identifiable { 128 | public var identifier: String { return String(hash) } 129 | } 130 | 131 | open class IdentityMap { 132 | var dictionary = [String: T]() 133 | let accessQueue = DispatchQueue(label: "com.mochxiao.isolation.queue", attributes: .concurrent) 134 | 135 | open subscript(identifier: String) -> T? { 136 | var result: T? = nil 137 | accessQueue.sync { result = self.dictionary[identifier] as T? } 138 | return result 139 | } 140 | 141 | func add(_ object: T) { 142 | accessQueue.async(flags: .barrier) { 143 | self.dictionary[object.identifier] = object 144 | } 145 | } 146 | } 147 | 148 | -------------------------------------------------------------------------------- /Sources/Extension/UIColor+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIColor+Extension.swift 3 | // Copyright (c) 2015-2016 Red Rain (http://mochxiao.com). 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | // THE SOFTWARE. 22 | // 23 | 24 | import UIKit 25 | 26 | public extension UIColor { 27 | public class var random: UIColor { 28 | let red = CGFloat(ExtensionKit.random(in: 0 ..< 255)) 29 | let green = CGFloat(ExtensionKit.random(in: 0 ..< 255)) 30 | let blue = CGFloat(ExtensionKit.random(in: 0 ..< 255)) 31 | return UIColor.make(red: red, green: green, blue: blue) 32 | } 33 | 34 | public class func make(red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat = 100) -> UIColor { 35 | return UIColor(red: red / 255.0, green: green / 255.0, blue: blue / 255.0, alpha: alpha / 100) 36 | } 37 | 38 | public class func make(hex: String, alpha: CGFloat = 100) -> UIColor { 39 | // Convert hex string to an integer 40 | var hexint: UInt32 = 0 41 | 42 | // Create scanner 43 | let scanner = Scanner(string: hex) 44 | 45 | // Tell scanner to skip the # character 46 | scanner.charactersToBeSkipped = CharacterSet(charactersIn: "#") 47 | scanner.scanHexInt32(&hexint) 48 | 49 | // Create color object, specifying alpha 50 | if hex.count <= 4 { 51 | let divisor = CGFloat(15) 52 | let red = CGFloat((hexint & 0xF00) >> 8) / divisor 53 | let green = CGFloat((hexint & 0x0F0) >> 4) / divisor 54 | let blue = CGFloat( hexint & 0x00F ) / divisor 55 | return UIColor(red: red, green: green, blue: blue, alpha: alpha / 100) 56 | } else { 57 | let divisor = CGFloat(255) 58 | let red = CGFloat((hexint & 0xFF0000) >> 16) / divisor 59 | let green = CGFloat((hexint & 0xFF00 ) >> 8) / divisor 60 | let blue = CGFloat( hexint & 0xFF ) / divisor 61 | return UIColor(red: red, green: green, blue: blue, alpha: alpha / 100) 62 | } 63 | } 64 | } 65 | 66 | public func UIColorFrom(red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat = 100) -> UIColor { 67 | return UIColor.make(red: red, green: green, blue: blue, alpha: alpha) 68 | } 69 | 70 | public func UIColorFrom(hex: String, alpha: CGFloat = 100) -> UIColor { 71 | return UIColor.make(hex: hex, alpha: alpha) 72 | } 73 | 74 | // MARK: - iOS default color 75 | 76 | public extension UIColor { 77 | public class var tint: UIColor { 78 | // 3, 122, 255, 100 79 | return UIColor.make(hex: "037AFF") 80 | } 81 | 82 | public class var separator: UIColor { 83 | // 200, 199, 204, 100 84 | return UIColor.make(hex: "C8C7CC") 85 | } 86 | 87 | public class var separatorDark: UIColor { 88 | // 69, 75, 65, 100 89 | return UIColor.make(hex: "454b41") 90 | } 91 | 92 | /// Grouped table view background. 93 | public class var groupedBackground: UIColor { 94 | // 239, 239, 244, 100 95 | return UIColor.make(hex: "EFEFF4") 96 | } 97 | 98 | /// Activity background 99 | public class var activityBackground: UIColor { 100 | // 248, 248, 248, 60 101 | return UIColor.make(hex: "F8F8F8", alpha: 60) 102 | } 103 | 104 | public class var disclosureIndicator: UIColor { 105 | return UIColor.make(hex: "C7C7CC") 106 | } 107 | 108 | /// Navigation bar title. 109 | public class var naviTitle: UIColor { 110 | // 3, 3, 3, 100 111 | return UIColor.make(hex: "030303") 112 | } 113 | 114 | public class var subTitle: UIColor { 115 | // 144, 144, 148, 100 116 | return UIColor.make(hex: "909094") 117 | } 118 | 119 | public class var placeholder: UIColor { 120 | // 200, 200, 205, 100 121 | return UIColor.make(hex: "C8C8CD") 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /Sources/Extension/UIFont+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIFont+Extension.swift 3 | // Copyright (c) 2015-2016 Red Rain (http://mochxiao.com). 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | // THE SOFTWARE. 22 | // 23 | 24 | import UIKit 25 | 26 | // MARK: - Fonts 27 | 28 | public extension UIKit.UIFont { 29 | public class var size8Fixed: UIFont { 30 | return UIFont.systemFont(ofSize: 8.0) 31 | } 32 | 33 | public class var size9Fixed: UIFont { 34 | return UIFont.systemFont(ofSize: 9.0) 35 | } 36 | 37 | public class var size10Fixed: UIFont { 38 | return UIFont.systemFont(ofSize: 10.0) 39 | } 40 | 41 | public class var caption2: UIFont { 42 | return UIFont.preferredFont(forTextStyle: UIFontTextStyle.caption2) 43 | } 44 | 45 | public class var size11: UIFont { 46 | return UIFont.caption2 47 | } 48 | 49 | public class var caption1: UIFont { 50 | return UIFont.preferredFont(forTextStyle: UIFontTextStyle.caption1) 51 | } 52 | 53 | public class var size12: UIFont { 54 | return UIFont.caption1 55 | } 56 | 57 | public class var footnote: UIFont { 58 | return UIFont.preferredFont(forTextStyle: UIFontTextStyle.footnote) 59 | } 60 | 61 | public class var size13: UIFont { 62 | return UIFont.footnote 63 | } 64 | 65 | public class var size14Fixed: UIFont { 66 | return UIFont.systemFont(ofSize: 14.0) 67 | } 68 | 69 | public class var subheadline: UIFont { 70 | return UIFont.preferredFont(forTextStyle: UIFontTextStyle.subheadline) 71 | } 72 | 73 | public class var size15: UIFont { 74 | return UIFont.subheadline 75 | } 76 | 77 | public class var body: UIFont { 78 | return UIFont.preferredFont(forTextStyle: UIFontTextStyle.body) 79 | } 80 | 81 | public class var size17: UIFont { 82 | return UIFont.body 83 | } 84 | 85 | public class var headline: UIFont { 86 | return UIFont.preferredFont(forTextStyle: UIFontTextStyle.headline) 87 | } 88 | 89 | public class var size17Blod: UIFont { 90 | return UIFont.headline 91 | } 92 | 93 | public class var size18Fixed: UIFont { 94 | return UIFont.systemFont(ofSize: 18.0) 95 | } 96 | 97 | public class var size19Fixed: UIFont { 98 | return UIFont.systemFont(ofSize: 19.0) 99 | } 100 | 101 | public class var size20Fixed: UIFont { 102 | return UIFont.systemFont(ofSize: 20.0) 103 | } 104 | 105 | public class var size21Fixed: UIFont { 106 | return UIFont.systemFont(ofSize: 21.0) 107 | } 108 | 109 | public class var size22Fixed: UIFont { 110 | return UIFont.systemFont(ofSize: 22.0) 111 | } 112 | 113 | public class var size23Fixed: UIFont { 114 | return UIFont.systemFont(ofSize: 23.0) 115 | } 116 | 117 | public class var size24Fixed: UIFont { 118 | return UIFont.systemFont(ofSize: 24.0) 119 | } 120 | 121 | public class var size26Fixed: UIFont { 122 | return UIFont.systemFont(ofSize: 26.0) 123 | } 124 | 125 | public class var size28Fixed: UIFont { 126 | return UIFont.systemFont(ofSize: 28.0) 127 | } 128 | 129 | public class var size30Fixed: UIFont { 130 | return UIFont.systemFont(ofSize: 30.0) 131 | } 132 | 133 | public class var size32Fixed: UIFont { 134 | return UIFont.systemFont(ofSize: 32.0) 135 | } 136 | 137 | public class var size34Fixed: UIFont { 138 | return UIFont.systemFont(ofSize: 34.0) 139 | } 140 | 141 | public class var size36Fixed: UIFont { 142 | return UIFont.systemFont(ofSize: 36.0) 143 | } 144 | 145 | public class var size38Fixed: UIFont { 146 | return UIFont.systemFont(ofSize: 38.0) 147 | } 148 | 149 | public class var size40Fixed: UIFont { 150 | return UIFont.systemFont(ofSize: 40.0) 151 | } 152 | 153 | // MARK: - available 154 | 155 | public class var callout: UIFont { 156 | guard #available(iOS 9.0, *) else { fatalError("Not supported for current system version.") } 157 | return UIFont.preferredFont(forTextStyle: UIFontTextStyle.callout) 158 | } 159 | 160 | public class var title3: UIFont { 161 | guard #available(iOS 9.0, *) else { fatalError("Not supported for current system version.") } 162 | return UIFont.preferredFont(forTextStyle: UIFontTextStyle.title3) 163 | } 164 | 165 | public class var title2: UIFont { 166 | guard #available(iOS 9.0, *) else { fatalError("Not supported for current system version.") } 167 | return UIFont.preferredFont(forTextStyle: UIFontTextStyle.title2) 168 | } 169 | 170 | public class var title1: UIFont { 171 | guard #available(iOS 9.0, *) else { fatalError("Not supported for current system version.") } 172 | return UIFont.preferredFont(forTextStyle: UIFontTextStyle.title1) 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /Sources/Classes/BadgeView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BadgeView.swift 3 | // Copyright (c) 2015-2016 Red Rain (http://mochxiao.com). 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | // THE SOFTWARE. 22 | // 23 | 24 | import UIKit 25 | 26 | final public class BadgeView: UIView { 27 | fileprivate let fixedHeight: CGFloat = 18 28 | public var textColor: UIColor = UIColor.white 29 | 30 | let badgeLabel: UILabel = { 31 | let label = UILabel() 32 | label.textColor = UIColor.white 33 | label.font = UIFont.preferredFont(forTextStyle: UIFontTextStyle.footnote) 34 | label.lineBreakMode = .byTruncatingTail 35 | label.textAlignment = .center 36 | return label 37 | }() 38 | 39 | public var badgeValue: String = "" { 40 | didSet { 41 | badgeLabel.text = badgeValue 42 | var width: CGFloat = badgeValue.layoutSize(font: badgeLabel.font).width + fixedHeight / 2 43 | if width < fixedHeight { 44 | width = fixedHeight 45 | } 46 | 47 | self.width = ceil(width) 48 | self.height = fixedHeight 49 | displayIfNeeded() 50 | } 51 | } 52 | 53 | fileprivate func displayIfNeeded() { 54 | isHidden = badgeValue.count == 0 || badgeValue == "0" 55 | if !isHidden { 56 | setNeedsDisplay() 57 | invalidateIntrinsicContentSize() 58 | } 59 | } 60 | 61 | public override var intrinsicContentSize : CGSize { 62 | return CGSize(width: bounds.width, height: bounds.height) 63 | } 64 | 65 | public override func draw(_ rect: CGRect) { 66 | badgeLabel.backgroundColor = backgroundColor 67 | layer.cornerRadius = fixedHeight / 2 68 | layer.masksToBounds = true 69 | layer.shouldRasterize = true 70 | layer.rasterizationScale = UIScreen.main.scale 71 | badgeLabel.backgroundColor?.setFill() 72 | UIRectFill(rect) 73 | badgeLabel.drawText(in: rect) 74 | } 75 | } 76 | 77 | // MARK: 78 | 79 | public extension UIView { 80 | fileprivate struct AssociatedKey { 81 | static var badgeView: String = "badgeView" 82 | } 83 | 84 | fileprivate var badgeView: BadgeView? { 85 | get { return associatedObject(forKey: &AssociatedKey.badgeView) as? BadgeView } 86 | set { associate(assignObject: newValue, forKey: &AssociatedKey.badgeView) } 87 | } 88 | 89 | public var badge: String? { 90 | get { return badgeView?.badgeValue } 91 | set { 92 | if nil == badgeView { 93 | addBadgeView() 94 | } 95 | badgeView?.badgeValue = newValue ?? "" 96 | badgeView?.isUserInteractionEnabled = false 97 | } 98 | } 99 | 100 | public var badgeBackgroundColor: UIColor? { 101 | get { return badgeView?.backgroundColor } 102 | set { 103 | if nil == badgeView { 104 | addBadgeView() 105 | } 106 | badgeView?.backgroundColor = newValue 107 | } 108 | } 109 | 110 | public var badgeColor: UIColor? { 111 | get { return badgeView?.textColor } 112 | set { 113 | if nil == badgeView { 114 | addBadgeView() 115 | } 116 | badgeView?.textColor = newValue ?? UIColor.white 117 | } 118 | } 119 | 120 | public func setBadgeOffset(_ offset: UIOffset) { 121 | if nil == badgeView { 122 | addBadgeView() 123 | } 124 | guard let badgeView = badgeView else { return } 125 | 126 | var needsRemove = [NSLayoutConstraint]() 127 | for cons in constraints { 128 | if cons.firstItem as? NSObject == badgeView { 129 | needsRemove.append(cons) 130 | } 131 | } 132 | removeConstraints(needsRemove) 133 | 134 | addConstraint(NSLayoutConstraint( 135 | item: badgeView, 136 | attribute: .centerX, 137 | relatedBy: .equal, 138 | toItem: self, 139 | attribute: .right, 140 | multiplier: 1, 141 | constant: offset.horizontal 142 | )) 143 | addConstraint(NSLayoutConstraint( 144 | item: badgeView, 145 | attribute: .centerY, 146 | relatedBy: .equal, 147 | toItem: self, 148 | attribute: .top, 149 | multiplier: 1, 150 | constant: offset.vertical 151 | )) 152 | } 153 | 154 | fileprivate func addBadgeView() { 155 | let badgeView = BadgeView() 156 | badgeView.translatesAutoresizingMaskIntoConstraints = false 157 | badgeView.backgroundColor = UIColor.red 158 | 159 | addSubview(badgeView) 160 | addConstraint(NSLayoutConstraint( 161 | item: badgeView, 162 | attribute: .centerX, 163 | relatedBy: .equal, 164 | toItem: self, 165 | attribute: .right, 166 | multiplier: 1, 167 | constant: 0 168 | )) 169 | addConstraint(NSLayoutConstraint( 170 | item: badgeView, 171 | attribute: .centerY, 172 | relatedBy: .equal, 173 | toItem: self, 174 | attribute: .top, 175 | multiplier: 1, 176 | constant: 0 177 | )) 178 | self.badgeView = badgeView 179 | } 180 | 181 | } 182 | 183 | 184 | -------------------------------------------------------------------------------- /Sources/Extension/UIScrollView+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIScrollView+Extension.swift 3 | // Copyright (c) 2015-2016 Red Rain (http://mochxiao.com). 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | // THE SOFTWARE. 22 | // 23 | 24 | import UIKit 25 | 26 | public extension UIKit.UIScrollView { 27 | public var insetTop: CGFloat { 28 | get { return contentInset.top } 29 | set { 30 | var inset = contentInset 31 | inset.top = newValue 32 | contentInset = inset 33 | } 34 | } 35 | 36 | public var insetLeft: CGFloat { 37 | get { return contentInset.left } 38 | set { 39 | var inset = contentInset 40 | inset.left = newValue 41 | contentInset = inset 42 | } 43 | } 44 | 45 | public var insetBottom: CGFloat { 46 | get { return contentInset.bottom } 47 | set { 48 | var inset = contentInset 49 | inset.bottom = newValue 50 | contentInset = inset 51 | } 52 | } 53 | 54 | public var insetRight: CGFloat { 55 | get { return contentInset.right } 56 | set { 57 | var inset = contentInset 58 | inset.right = newValue 59 | contentInset = inset 60 | } 61 | } 62 | 63 | public var scrollIndicatorInsetTop: CGFloat { 64 | get { return scrollIndicatorInsets.top } 65 | set { 66 | 67 | var inset = scrollIndicatorInsets 68 | inset.top = newValue 69 | scrollIndicatorInsets = inset 70 | } 71 | } 72 | 73 | public var scrollIndicatorInsetLeft: CGFloat { 74 | get { return scrollIndicatorInsets.left } 75 | set { 76 | var inset = scrollIndicatorInsets 77 | inset.left = newValue 78 | scrollIndicatorInsets = inset 79 | } 80 | } 81 | 82 | public var scrollIndicatorInsetBottom: CGFloat { 83 | get { return scrollIndicatorInsets.bottom } 84 | set { 85 | var inset = scrollIndicatorInsets 86 | inset.bottom = newValue 87 | scrollIndicatorInsets = inset 88 | } 89 | } 90 | 91 | public var scrollIndicatorInsetRight: CGFloat { 92 | get { return scrollIndicatorInsets.right } 93 | set { 94 | var inset = scrollIndicatorInsets 95 | inset.right = newValue 96 | scrollIndicatorInsets = inset 97 | } 98 | } 99 | 100 | public var contentOffsetX: CGFloat { 101 | get { return contentOffset.x } 102 | set { 103 | var offset = contentOffset 104 | offset.x = newValue 105 | contentOffset = offset 106 | } 107 | } 108 | 109 | public var contentOffsetY: CGFloat { 110 | get { return contentOffset.y } 111 | set { 112 | var offset = contentOffset 113 | offset.y = newValue 114 | contentOffset = offset 115 | } 116 | } 117 | 118 | public var contentSizeWidth: CGFloat { 119 | get { return contentSize.width } 120 | set { 121 | var size = contentSize 122 | size.width = newValue 123 | contentSize = size 124 | } 125 | } 126 | 127 | public var contentSizeHeight: CGFloat { 128 | get { return contentSize.height } 129 | set { 130 | var size = contentSize 131 | size.height = newValue 132 | contentSize = size 133 | } 134 | } 135 | } 136 | 137 | // MARK: - RefreshControl 138 | 139 | public extension UIScrollView { 140 | fileprivate struct AssociationKey { 141 | fileprivate static var refreshControl: String = "com.mochxiao.uiscrollview.RefreshControl" 142 | } 143 | 144 | public private(set) var refreshContrl: UIRefreshControl? { 145 | get { return associatedObject(forKey: &AssociationKey.refreshControl) as? UIRefreshControl } 146 | set { associate(assignObject: newValue, forKey: &AssociationKey.refreshControl) } 147 | } 148 | 149 | public func addRefreshControl(withActionHandler handler: @escaping ((UIScrollView) -> ())) { 150 | if let _ = refreshContrl { 151 | return 152 | } 153 | 154 | let _refreshContrl = UIRefreshControl(frame: CGRect( 155 | x: 0, 156 | y: 0, 157 | width: UIScreen.main.bounds.width, 158 | height: 64 159 | )) 160 | addSubview(_refreshContrl) 161 | sendSubview(toBack: _refreshContrl) 162 | _refreshContrl.addControlEvents(.valueChanged) { [weak self] (_) in 163 | if let this = self { 164 | if this.refreshControlEnabled { 165 | handler(this) 166 | } else { 167 | this.endRefreshing() 168 | } 169 | } 170 | } 171 | refreshContrl = _refreshContrl 172 | } 173 | 174 | public var refreshControlEnabled: Bool { 175 | get { 176 | if let refreshContrl = refreshContrl { 177 | return refreshContrl.isEnabled 178 | } 179 | return false 180 | } 181 | set { 182 | if let refreshContrl = refreshContrl { 183 | refreshContrl.isEnabled = newValue 184 | refreshContrl.alpha = newValue ? 1 : 0 185 | } 186 | } 187 | } 188 | 189 | public func beginRefreshing() { 190 | refreshContrl?.beginRefreshing() 191 | } 192 | 193 | public func endRefreshing() { 194 | refreshContrl?.endRefreshing() 195 | } 196 | 197 | public var refreshing: Bool { 198 | if let refreshContrl = refreshContrl { 199 | return refreshContrl.isRefreshing 200 | } 201 | return false 202 | } 203 | } 204 | 205 | // MARK: - For YYLabel touch hightlight 206 | 207 | public extension UIScrollView { 208 | public func commitTouchesImmediately() { 209 | delaysContentTouches = false 210 | canCancelContentTouches = true 211 | 212 | if let wrapView = subviews.first { 213 | if NSStringFromClass(type(of: wrapView)).hasSuffix("WrapperView") { 214 | if let gestureRecognizers = wrapView.gestureRecognizers { 215 | for gesture in gestureRecognizers { 216 | if NSStringFromClass(type(of: gesture)).contains("DelayedTouchesBegan") { 217 | gesture.isEnabled = false 218 | break 219 | } 220 | } 221 | } 222 | } 223 | } 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /Sources/Extension/UIControl+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIControl+Extension.swift 3 | // Copyright (c) 2015-2016 Red Rain (http://mochxiao.com). 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | // THE SOFTWARE. 22 | // 23 | 24 | import UIKit 25 | 26 | private struct AssociationKey { 27 | fileprivate static var touchDownHandlerWrapper: String = "com.mochxiao.uicontrol.touchDownHandlerWrapper" 28 | fileprivate static var touchDownRepeatHandlerWrapper: String = "com.mochxiao.uicontrol.touchDownRepeatHandlerWrapper" 29 | fileprivate static var touchDragInsideHandlerWrapper: String = "com.mochxiao.uicontrol.touchDragInsideHandlerWrapper" 30 | fileprivate static var touchDragOutsideHandlerWrapper: String = "com.mochxiao.uicontrol.touchDragOutsideHandlerWrapper" 31 | fileprivate static var touchDragEnterHandlerWrapper: String = "com.mochxiao.uicontrol.touchDragEnterHandlerWrapper" 32 | fileprivate static var touchDragExitHandlerWrapper: String = "com.mochxiao.uicontrol.touchDragExitHandlerWrapper" 33 | fileprivate static var touchUpInsideHandlerWrapper: String = "com.mochxiao.uicontrol.touchUpInsideHandlerWrapper" 34 | fileprivate static var touchUpOutsideHandlerWrapper: String = "com.mochxiao.uicontrol.touchUpOutsideHandlerWrapper" 35 | fileprivate static var touchCancelHandlerWrapper: String = "com.mochxiao.uicontrol.touchCancelHandlerWrapper" 36 | fileprivate static var valueChangedHandlerWrapper: String = "com.mochxiao.uicontrol.valueChangedHandlerWrapper" 37 | fileprivate static var primaryActionTriggeredHandlerWrapper: String = "com.mochxiao.uicontrol.primaryActionTriggeredHandlerWrapper" 38 | fileprivate static var editingDidBeginHandlerWrapper: String = "com.mochxiao.uicontrol.editingDidBeginHandlerWrapper" 39 | fileprivate static var editingChangedHandlerWrapper: String = "com.mochxiao.uicontrol.editingChangedHandlerWrapper" 40 | fileprivate static var editingDidEndHandlerWrapper: String = "com.mochxiao.uicontrol.editingDidEndHandlerWrapper" 41 | fileprivate static var editingDidEndOnExitHandlerWrapper: String = "com.mochxiao.uicontrol.editingDidEndOnExitHandlerWrapper" 42 | fileprivate static var allTouchEventsHandlerWrapper: String = "com.mochxiao.uicontrol.allTouchEventsHandlerWrapper" 43 | fileprivate static var allEditingEventsHandlerWrapper: String = "com.mochxiao.uicontrol.allEditingEventsHandlerWrapper" 44 | fileprivate static var applicationReservedHandlerWrapper: String = "com.mochxiao.uicontrol.applicationReservedHandlerWrapper" 45 | fileprivate static var systemReservedHandlerWrapper: String = "com.mochxiao.uicontrol.systemReservedHandlerWrapper" 46 | fileprivate static var allEventsHandlerWrapper: String = "com.mochxiao.uicontrol.allEventsHandlerWrapper" 47 | } 48 | 49 | // MARK: - UIControl Action 50 | 51 | public protocol UIControlActionFunctionProtocol {} 52 | extension UIControl: UIControlActionFunctionProtocol {} 53 | 54 | public extension UIControlActionFunctionProtocol where Self: UIControl { 55 | public func addControlEvents(_ events: UIControlEvents, handler: @escaping (Self) -> ()) { 56 | let trampoline = ActionTrampoline(action: handler) 57 | addTarget(trampoline, action: NSSelectorFromString("action:"), for: events) 58 | associate(object: trampoline, forEvents: events) 59 | } 60 | 61 | fileprivate func associate(object: AnyObject, forEvents events: UIControlEvents) { 62 | if events.contains(.touchDown) { 63 | associate(retainObject: object, forKey: &AssociationKey.touchDownHandlerWrapper) 64 | } 65 | if events.contains(.touchDownRepeat) { 66 | associate(retainObject: object, forKey: &AssociationKey.touchDownRepeatHandlerWrapper) 67 | } 68 | if events.contains(.touchDragInside) { 69 | associate(retainObject: object, forKey: &AssociationKey.touchDragInsideHandlerWrapper) 70 | } 71 | if events.contains(.touchDragOutside) { 72 | associate(retainObject: object, forKey: &AssociationKey.touchDragOutsideHandlerWrapper) 73 | } 74 | if events.contains(.touchDragEnter) { 75 | associate(retainObject: object, forKey: &AssociationKey.touchDragEnterHandlerWrapper) 76 | } 77 | if events.contains(.touchDragExit) { 78 | associate(retainObject: object, forKey: &AssociationKey.touchDragExitHandlerWrapper) 79 | } 80 | if events.contains(.touchUpInside) { 81 | associate(retainObject: object, forKey: &AssociationKey.touchUpInsideHandlerWrapper) 82 | } 83 | if events.contains(.touchUpOutside) { 84 | associate(retainObject: object, forKey: &AssociationKey.touchUpOutsideHandlerWrapper) 85 | } 86 | if events.contains(.touchCancel) { 87 | associate(retainObject: object, forKey: &AssociationKey.touchCancelHandlerWrapper) 88 | } 89 | if events.contains(.valueChanged) { 90 | associate(retainObject: object, forKey: &AssociationKey.valueChangedHandlerWrapper) 91 | } 92 | if events.contains(.editingDidBegin) { 93 | associate(retainObject: object, forKey: &AssociationKey.editingDidBeginHandlerWrapper) 94 | } 95 | if events.contains(.editingChanged) { 96 | associate(retainObject: object, forKey: &AssociationKey.editingChangedHandlerWrapper) 97 | } 98 | if events.contains(.editingDidEnd) { 99 | associate(retainObject: object, forKey: &AssociationKey.editingDidEndHandlerWrapper) 100 | } 101 | if events.contains(.allTouchEvents) { 102 | associate(retainObject: object, forKey: &AssociationKey.allTouchEventsHandlerWrapper) 103 | } 104 | if events.contains(.editingDidEndOnExit) { 105 | associate(retainObject: object, forKey: &AssociationKey.editingDidEndOnExitHandlerWrapper) 106 | } 107 | if events.contains(.allEditingEvents) { 108 | associate(retainObject: object, forKey: &AssociationKey.allEditingEventsHandlerWrapper) 109 | } 110 | if events.contains(.applicationReserved) { 111 | associate(retainObject: object, forKey: &AssociationKey.applicationReservedHandlerWrapper) 112 | } 113 | if events.contains(.systemReserved) { 114 | associate(retainObject: object, forKey: &AssociationKey.systemReservedHandlerWrapper) 115 | } 116 | if events.contains(.allEvents) { 117 | associate(retainObject: object, forKey: &AssociationKey.allEventsHandlerWrapper) 118 | } 119 | 120 | if #available(iOS 9.0, *) { 121 | if events.contains(.primaryActionTriggered) { 122 | associate(retainObject: object, forKey: &AssociationKey.primaryActionTriggeredHandlerWrapper) 123 | } 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /Sources/Extension/Lang+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Lang+Extension.swift 3 | // Copyright (c) 2015-2016 Red Rain (http://mochxiao.com). 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | // THE SOFTWARE. 22 | // 23 | 24 | import Foundation 25 | 26 | public func +(lhs: UIEdgeInsets, rhs: UIEdgeInsets) -> UIEdgeInsets { 27 | return UIEdgeInsetsMake(lhs.top + rhs.top, lhs.left + rhs.left, lhs.bottom + rhs.bottom, lhs.right + rhs.right) 28 | } 29 | 30 | /// Find out the geiven object is some type or not. 31 | public func objectIsType(_ object: Any, _ someObjectOfType: T.Type) -> Bool { 32 | return object is T 33 | } 34 | 35 | /** 36 | See: https://gist.githubusercontent.com/Abizern/a81f31a75e1ad98ff80d/raw/85b85cbb9bcdeb8cdbf2521ac935e3d2cb4cdd4f/loggingPrint.swift 37 | 38 | Prints the filename, function name, line number and textual representation of `object` and a newline character into 39 | the standard output if the build setting for "Other Swift Flags" defines `-D DEBUG`. 40 | 41 | The current thread is a prefix on the output. for the main thread, for anything else. 42 | 43 | Only the first parameter needs to be passed to this funtion. 44 | 45 | The textual representation is obtained from the `object` using its protocol conformances, in the following 46 | order of preference: `CustomDebugStringConvertible` and `CustomStringConvertible`. Do not overload this function for 47 | your type. Instead, adopt one of the protocols mentioned above. 48 | 49 | :param: object The object whose textual representation will be printed. If this is an expression, it is lazily evaluated. 50 | :param: file The name of the file, defaults to the current file without the ".swift" extension. 51 | :param: function The name of the function, defaults to the function within which the call is made. 52 | :param: line The line number, defaults to the line number within the file that the call is made. 53 | */ 54 | public func logging(_ object: @autoclosure () -> T, _ file: String = #file, _ function: String = #function, _ line: Int = #line) { 55 | #if DEBUG 56 | let value = object() 57 | let stringRepresentation: String 58 | 59 | if let value = value as? CustomDebugStringConvertible { 60 | stringRepresentation = value.debugDescription 61 | } else if let value = value as? CustomStringConvertible { 62 | stringRepresentation = value.description 63 | } else { 64 | stringRepresentation = "\(value)" 65 | } 66 | let fileURL = URL(string: file)?.lastPathComponent ?? "Unknown file" 67 | let queue = Thread.isMainThread ? "UI" : "BG" 68 | print("<\(queue)> \(fileURL) \(function)[\(line)]: " + stringRepresentation) 69 | #endif 70 | } 71 | 72 | /// Get value from `any` instance like KVC 73 | public func takeValue(from object: Any, forKey key: String) -> Any? { 74 | func takeValue(_ mirror: Mirror, _ key: String) -> Any? { 75 | for (targetKey, targetMirror) in mirror.children { 76 | if key == targetKey { 77 | return targetMirror 78 | } 79 | } 80 | return nil 81 | } 82 | 83 | func take(_ mirror: Mirror, _ key: String) -> Any? { 84 | if let value = takeValue(mirror, key) { 85 | return value 86 | } 87 | if let superMirror = mirror.superclassMirror { 88 | return take(superMirror, key) 89 | } 90 | return nil 91 | } 92 | 93 | let mirror = Mirror(reflecting: object) 94 | return take(mirror, key) 95 | } 96 | 97 | /// Generate random number in range 98 | public func random(in range: Range) -> Int { 99 | let count = UInt32(range.upperBound - range.lowerBound) 100 | return Int(arc4random_uniform(count)) + range.lowerBound 101 | } 102 | 103 | // MARK: - synchronized 104 | 105 | public func synchronized(lock: AnyObject, work: () -> ()) { 106 | objc_sync_enter(lock) 107 | work() 108 | objc_sync_exit(lock) 109 | } 110 | 111 | // MARK: - Method swizzle 112 | 113 | /// Should be placed in dispatch_once 114 | public func swizzleInstanceMethod( 115 | for cls: AnyClass, 116 | original: Selector, 117 | override: Selector) 118 | { 119 | guard let originalMethod = class_getInstanceMethod(cls, original) else { return } 120 | guard let overrideMethod = class_getInstanceMethod(cls, override) else { return } 121 | 122 | if class_addMethod( 123 | cls, 124 | original, 125 | method_getImplementation(overrideMethod), 126 | method_getTypeEncoding(overrideMethod)) 127 | { 128 | class_replaceMethod( 129 | cls, 130 | override, 131 | method_getImplementation(originalMethod), 132 | method_getTypeEncoding(originalMethod) 133 | ) 134 | } else { 135 | method_exchangeImplementations(originalMethod, overrideMethod) 136 | } 137 | } 138 | 139 | /// Should be placed in dispatch_once 140 | public func swizzleClassMethod( 141 | for cls: AnyClass, 142 | original: Selector, 143 | override: Selector) 144 | { 145 | guard let originalMethod = class_getClassMethod(cls, original) else { return } 146 | guard let overrideMethod = class_getClassMethod(cls, override) else { return } 147 | 148 | if class_addMethod( 149 | cls, 150 | original, 151 | method_getImplementation(overrideMethod), 152 | method_getTypeEncoding(overrideMethod)) 153 | { 154 | class_replaceMethod( 155 | cls, 156 | override, 157 | method_getImplementation(originalMethod), 158 | method_getTypeEncoding(originalMethod) 159 | ) 160 | } else { 161 | method_exchangeImplementations(originalMethod, overrideMethod) 162 | } 163 | } 164 | 165 | // MARK: - C Pointers 166 | 167 | /// Convert a `void *` type to Swift type, use this function carefully 168 | public func convertUnsafePointerToSwiftType(_ value: UnsafeRawPointer) -> T { 169 | return value.assumingMemoryBound(to: T.self).pointee 170 | } 171 | 172 | // MARK: - Sandbox 173 | 174 | private func searchPath(for directory: FileManager.SearchPathDirectory) -> String? { 175 | return NSSearchPathForDirectoriesInDomains(directory, FileManager.SearchPathDomainMask.userDomainMask, true).first 176 | } 177 | 178 | public func directoryForDocument() -> String? { 179 | return searchPath(for: .documentDirectory) 180 | } 181 | 182 | public func directoryForCache() -> String? { 183 | return searchPath(for: .cachesDirectory) 184 | } 185 | 186 | public func directoryForDownloads() -> String? { 187 | return searchPath(for: .downloadsDirectory) 188 | } 189 | 190 | public func directoryForMovies() -> String? { 191 | return searchPath(for: .moviesDirectory) 192 | } 193 | 194 | public func directoryForMusic() -> String? { 195 | return searchPath(for: .musicDirectory) 196 | } 197 | 198 | public func directoryForPictures() -> String? { 199 | return searchPath(for: .picturesDirectory) 200 | } 201 | 202 | // MARK: - ClosureDecorator 203 | 204 | /// ClosureDecorator, make use closure like a NSObject, aka objc_asscoiateXXX. 205 | final public class ClosureDecorator: NSObject { 206 | fileprivate let closure: Any 207 | 208 | fileprivate override init() { 209 | fatalError("Use init(action:) instead.") 210 | } 211 | 212 | public init(_ closure: @escaping (() -> ())) { 213 | self.closure = closure 214 | } 215 | 216 | public init(_ closure: @escaping ((T) -> ())) { 217 | self.closure = closure 218 | } 219 | 220 | public func invoke(_ param: T) { 221 | if let closure = closure as? (() -> ()) { 222 | closure() 223 | } else if let closure = closure as? ((T) -> ()) { 224 | closure(param) 225 | } 226 | } 227 | 228 | deinit { 229 | logging("\(#file):\(#line):\(type(of: self)):\(#function)") 230 | } 231 | } 232 | 233 | // MARK: - Swifty Target & Action 234 | // See: https://www.mikeash.com/pyblog/friday-qa-2015-12-25-swifty-targetaction.html 235 | 236 | final public class ActionTrampoline: NSObject { 237 | fileprivate let action: ((T) -> ()) 238 | public var selector: Selector? { 239 | return NSSelectorFromString("action:") 240 | } 241 | 242 | public init(action: @escaping ((T) -> ())) { 243 | self.action = action 244 | } 245 | 246 | @objc public func action(_ sender: AnyObject) { 247 | // UIControl: add(target: AnyObject?, action: Selector, forControlEvents controlEvents: UIControlEvents) 248 | if let sender = sender as? T { 249 | action(sender) 250 | } 251 | // UIGestureRecognizer: add(target: AnyObject, action: Selector) 252 | else if let sender = sender as? UIGestureRecognizer { 253 | action(sender.view as! T) 254 | } 255 | } 256 | 257 | deinit { 258 | logging("\(#file):\(#line):\(type(of: self)):\(#function)") 259 | } 260 | } 261 | 262 | -------------------------------------------------------------------------------- /Sources/Extension/UIButton+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIButton+Extension.swift 3 | // Copyright (c) 2015-2016 Red Rain (http://mochxiao.com). 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | // THE SOFTWARE. 22 | // 23 | 24 | import UIKit 25 | 26 | // MARK: - AssociationKey 27 | 28 | private struct AssociationKey { 29 | fileprivate static var activityIndicatorContainerView: String = "com.mochxiao.uibutton.activityIndicatorContainerView" 30 | } 31 | 32 | // MARK: - Property for state 33 | 34 | public extension UIButton { 35 | public var title: String? { 36 | get { return self.title(for: .normal) } 37 | set { setTitle(newValue, for: .normal) } 38 | } 39 | 40 | public var titleFont: UIFont? { 41 | get { return titleLabel?.font } 42 | set { titleLabel?.font = newValue } 43 | } 44 | 45 | public var attributedTitle: NSAttributedString? { 46 | get { return self.attributedTitle(for: .normal) } 47 | set { setAttributedTitle(newValue, for: .normal) } 48 | } 49 | 50 | public var titleColor: UIColor? { 51 | get { return self.titleColor(for: .normal) } 52 | set { 53 | setTitleColor(newValue, for: .normal) 54 | setTitleColor(newValue?.withAlphaComponent(0.5), for: .disabled) 55 | setTitleColor(newValue, for: .selected) 56 | if buttonType == .custom { 57 | setTitleColor(newValue?.withAlphaComponent(0.5), for: .highlighted) 58 | } 59 | } 60 | } 61 | 62 | public var titleShadowColor: UIColor? { 63 | get { return self.titleShadowColor(for: .normal) } 64 | set { 65 | setTitleShadowColor(newValue, for: .normal) 66 | setTitleShadowColor(newValue?.withAlphaComponent(0.5), for: .disabled) 67 | setTitleShadowColor(newValue, for: .selected) 68 | } 69 | } 70 | 71 | public var image: UIImage? { 72 | get { return self.image(for: .normal) } 73 | set { 74 | setImage(newValue?.withRenderingMode(.alwaysOriginal), for: .normal) 75 | } 76 | } 77 | 78 | public var selectedImage: UIImage? { 79 | get { return self.image(for: .selected) } 80 | set { setImage(newValue?.withRenderingMode(.alwaysOriginal), for: .selected) } 81 | } 82 | 83 | public var backgroundImage: UIImage? { 84 | get { return self.backgroundImage(for: .normal) } 85 | set { 86 | let image = newValue?.withRenderingMode(.alwaysOriginal) 87 | setBackgroundImage(image, for: .normal) 88 | if buttonType == .custom { 89 | setBackgroundImage(image?.remake(alpha: 0.5), for: .highlighted) 90 | setBackgroundImage(image?.remake(alpha: 0.5), for: .disabled) 91 | } 92 | } 93 | } 94 | 95 | public var selectedBackgroundImage: UIImage? { 96 | get { return self.backgroundImage(for: .selected) } 97 | set { setBackgroundImage(newValue?.withRenderingMode(.alwaysOriginal), for: .selected) } 98 | } 99 | 100 | public var disabledBackgroundImage: UIImage? { 101 | get { return self.backgroundImage(for: .disabled) } 102 | set { setBackgroundImage(newValue?.withRenderingMode(.alwaysOriginal), for: .disabled) } 103 | } 104 | } 105 | 106 | // MARK: - Image position 107 | 108 | public extension UIButton { 109 | /// Convenience `setImageAlignmentToTop:` setter. 110 | public var imageAlignmentTopSpace: CGFloat { 111 | get { fatalError("Unavailable.") } 112 | set { setImageAlignmentToTop(titleSpace: newValue) } 113 | } 114 | 115 | /// Convenience `setImageAlignmentToLeft:` setter. 116 | public var imageAlignmentLeftSpace: CGFloat { 117 | get { fatalError("Unavailable.") } 118 | set { setImageAlignmentToLeft(titleSpace: newValue) } 119 | } 120 | 121 | /// Convenience `setImageAlignmentToBottom:` setter. 122 | public var imageAlignmentBottomSpace: CGFloat { 123 | get { fatalError("Unavailable.") } 124 | set { setImageAlignmentToBottom(titleSpace: newValue) } 125 | } 126 | 127 | /// Convenience `setImageAlignmentToRight:` setter. 128 | public var imageAlignmentRightSpace: CGFloat { 129 | get { fatalError("Unavailable.") } 130 | set { setImageAlignmentToRight(titleSpace: newValue) } 131 | } 132 | 133 | /// Setup image position relate to title 134 | /// **NOTE**: Before invoke this methods you should setup title and image already 135 | public func setImageAlignmentToTop(titleSpace space: CGFloat = 4.0) { 136 | guard let currentImage = currentImage else { return } 137 | guard let currentTitle = currentTitle as NSString? else { return } 138 | guard let titleLabel = titleLabel else { return } 139 | 140 | let halfSpace = (space / 2.0).ceilling 141 | let halfImageWidth = (currentImage.size.width / 2.0).ceilling 142 | let halfImageHeight = (currentImage.size.height / 2.0).ceilling 143 | titleEdgeInsets = UIEdgeInsetsMake( 144 | halfImageHeight + halfSpace, 145 | -halfImageWidth, 146 | -halfImageHeight - halfSpace, 147 | halfImageWidth 148 | ) 149 | 150 | let titleBounds = currentTitle.size(withAttributes: [.font: titleLabel.font]).ceilling 151 | let halfEdgeWidth = (titleBounds.width / 2.0).ceilling 152 | let halfEdgeHeight = (titleBounds.height / 2.0).ceilling 153 | imageEdgeInsets = UIEdgeInsetsMake( 154 | -halfEdgeHeight - halfSpace, 155 | halfEdgeWidth, 156 | halfEdgeHeight + halfSpace, 157 | -halfEdgeWidth 158 | ) 159 | } 160 | 161 | /// Setup image position relate to title 162 | /// **NOTE**: Before invoke this methods you should setup title and image already 163 | public func setImageAlignmentToBottom(titleSpace space: CGFloat = 4.0) { 164 | guard let currentImage = currentImage else { return } 165 | guard let currentTitle = currentTitle as NSString? else { return } 166 | guard let titleLabel = titleLabel else { return } 167 | 168 | let halfSpace = (space / 2.0).ceilling 169 | let halfImageWidth = (currentImage.size.width / 2.0).ceilling 170 | let halfImageHeight = (currentImage.size.height / 2.0).ceilling 171 | titleEdgeInsets = UIEdgeInsetsMake( 172 | -halfImageHeight - halfSpace, 173 | -halfImageWidth, 174 | halfImageHeight + halfSpace, 175 | halfImageWidth 176 | ) 177 | 178 | let titleBounds = currentTitle.size(withAttributes: [.font: titleLabel.font]).ceilling 179 | let halfEdgeWidth = (titleBounds.width / 2.0).ceilling 180 | let halfEdgeHeight = (titleBounds.height / 2.0).ceilling 181 | imageEdgeInsets = UIEdgeInsetsMake( 182 | halfEdgeHeight + halfSpace, 183 | halfEdgeWidth, 184 | -halfEdgeHeight - halfSpace, 185 | -halfEdgeWidth 186 | ) 187 | } 188 | 189 | /// Setup image position relate to title 190 | /// **NOTE**: Before invoke this methods you should setup title and image already 191 | public func setImageAlignmentToLeft(titleSpace space: CGFloat = 4.0) { 192 | let halfSpace = (space / 2.0).ceilling 193 | 194 | titleEdgeInsets = UIEdgeInsetsMake( 195 | 0, 196 | halfSpace, 197 | 0, 198 | -halfSpace 199 | ) 200 | imageEdgeInsets = UIEdgeInsetsMake( 201 | 0, 202 | -halfSpace, 203 | 0, 204 | halfSpace 205 | ) 206 | } 207 | 208 | /// Setup image position relate to title 209 | /// **NOTE**: Before invoke this methods you should setup title and image already 210 | public func setImageAlignmentToRight(titleSpace space: CGFloat = 4.0) { 211 | guard let currentImage = currentImage else { return } 212 | guard let currentTitle = currentTitle as NSString? else { return } 213 | guard let titleLabel = titleLabel else { return } 214 | 215 | let halfSpace = (space / 2.0).ceilling 216 | let imageWidth = currentImage.size.width.ceilling 217 | let edgeWidth = currentTitle.size(withAttributes: [.font: titleLabel.font]).width.ceilling 218 | 219 | titleEdgeInsets = UIEdgeInsetsMake( 220 | 0, 221 | -imageWidth - halfSpace, 222 | 0, 223 | imageWidth + halfSpace 224 | ) 225 | imageEdgeInsets = UIEdgeInsetsMake( 226 | 0, 227 | edgeWidth + halfSpace, 228 | 0, 229 | -edgeWidth - halfSpace 230 | ) 231 | } 232 | } 233 | 234 | // MARK: - 235 | 236 | public extension UIButton { 237 | public func performToggleSelectStateImageAnimation() { 238 | guard let normalImage = self.image(for: .normal) else { return } 239 | guard let selectedImage = self.image(for: .selected) else { return } 240 | guard let _imageView = imageView else { return } 241 | 242 | // Clear image 243 | { 244 | setImage(nil, for: .normal) 245 | setImage(nil, for: .selected) 246 | }() 247 | 248 | let animatedImageView = UIImageView(image: isSelected ? selectedImage : normalImage) 249 | animatedImageView.frame = _imageView.frame 250 | addSubview(animatedImageView) 251 | 252 | let recover = { 253 | UIView.animate(withDuration: 0.2, delay: 0, options: .beginFromCurrentState, animations: { 254 | animatedImageView.transform = CGAffineTransform.identity 255 | }, completion: { (finished: Bool) in 256 | self.setImage(normalImage, for: .normal) 257 | self.setImage(selectedImage, for: .selected) 258 | self.isSelected = !self.isSelected 259 | animatedImageView.removeFromSuperview() 260 | }) 261 | } 262 | 263 | let zoomOut = { 264 | animatedImageView.image = !self.isSelected ? selectedImage : normalImage 265 | UIView.animate(withDuration: 0.2, delay: 0, options: .beginFromCurrentState, animations: { 266 | animatedImageView.transform = CGAffineTransform(scaleX: 0.9, y: 0.9) 267 | }, completion: { (finished: Bool) in 268 | recover() 269 | }) 270 | } 271 | 272 | // Start with zoom in 273 | UIView.animate(withDuration: 0.2, delay: 0, options: .beginFromCurrentState, animations: { 274 | animatedImageView.transform = CGAffineTransform(scaleX: 1.7, y: 1.7) 275 | }, completion: { (finished: Bool) in 276 | zoomOut() 277 | }) 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /Sources/Extension/UIViewController+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewController+Extension.swift 3 | // Copyright (c) 2015-2016 Red Rain (http://mochxiao.com). 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | // THE SOFTWARE. 22 | // 23 | 24 | import UIKit 25 | 26 | // MARK: - AssociationKey 27 | 28 | private struct AssociationKey { 29 | fileprivate static var tag: String = "com.mochxiao.uialertaction.tag" 30 | fileprivate static var imagePickerCompletionHandlerWrapper = "com.mochxiao.uiimagepickercontroller.imagePickerCompletionHandlerWrapper" 31 | } 32 | 33 | // MARK: - Present UIAlertController 34 | 35 | public extension UIAlertAction { 36 | /// Default value is -1. 37 | public fileprivate(set) var tag: Int { 38 | get { 39 | if let value = associatedObject(forKey: &AssociationKey.tag) as? Int { 40 | return value 41 | } 42 | return -1 43 | } 44 | set { associate(retainObject: newValue, forKey: &AssociationKey.tag) } 45 | } 46 | } 47 | 48 | public extension UIViewController { 49 | /// Present error. 50 | public func presentError(_ error: NSError) { 51 | if let message = error.userInfo[NSLocalizedDescriptionKey] as? String { 52 | presentAlert(message: message) 53 | } else if let message = error.userInfo[NSLocalizedFailureReasonErrorKey] as? String { 54 | presentAlert(message: message) 55 | } else { 56 | presentAlert(message: error.localizedDescription) 57 | } 58 | } 59 | 60 | /// Present message. 61 | public func presentAlert( 62 | title: String = "", 63 | message: String, 64 | cancelTitle: String = "好", 65 | cancelHandler: ((UIAlertAction) -> ())? = nil, 66 | otherTitles: [String]? = nil, 67 | othersHandler: ((UIAlertAction) -> ())? = nil) 68 | { 69 | let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) 70 | let cancelAction = UIAlertAction(title: cancelTitle, style: .cancel, handler: cancelHandler) 71 | alertController.addAction(cancelAction) 72 | 73 | if let otherTitles = otherTitles { 74 | for (index, title) in otherTitles.enumerated() { 75 | let action = UIAlertAction(title: title, style: .default, handler: othersHandler) 76 | action.tag = index 77 | alertController.addAction(action) 78 | } 79 | } 80 | 81 | present(alertController, animated: true, completion: nil) 82 | } 83 | 84 | /// Present ActionSheet. 85 | public func presentActionSheet( 86 | title: String = "", 87 | message: String, 88 | cancelTitle: String = "取消", 89 | cancelHandler: ((UIAlertAction) -> ())? = nil, 90 | actionTitles: [String], 91 | actionHandler: ((UIAlertAction) -> ())? = nil) 92 | { 93 | let alertController = UIAlertController(title: title, message: message, preferredStyle: .actionSheet) 94 | for (index, title) in actionTitles.enumerated() { 95 | let action = UIAlertAction(title: title, style: .default, handler: actionHandler) 96 | action.tag = index 97 | alertController.addAction(action) 98 | } 99 | let cancelAction = UIAlertAction(title: cancelTitle, style: .cancel, handler: cancelHandler) 100 | alertController.addAction(cancelAction) 101 | present(alertController, animated: true, completion: nil) 102 | } 103 | } 104 | 105 | /// The convience version of `presentAlert(title:message:cancelTitle:cancelHandler:)`. 106 | /// Use this func carefully, it maybe iterate many times. 107 | public func doPresentAlert( 108 | title: String = "", 109 | message: String, 110 | cancelTitle: String = "好", 111 | cancelHandler: ((UIAlertAction) -> ())? = nil) 112 | { 113 | findLastPresentedViewController()?.presentAlert( 114 | title: title, 115 | message: message, 116 | cancelTitle: cancelTitle, 117 | cancelHandler: cancelHandler 118 | ) 119 | } 120 | 121 | public func doPresentAlert( 122 | title: String = "", 123 | message: String, 124 | cancelTitle: String = "好", 125 | cancelHandler: ((UIAlertAction) -> ())? = nil, 126 | actionTitles: [String], 127 | actionHandler: ((UIAlertAction) -> ())? = nil) 128 | { 129 | findLastPresentedViewController()?.presentAlert( 130 | title: title, 131 | message: message, 132 | cancelTitle: cancelTitle, 133 | cancelHandler: cancelHandler, 134 | otherTitles: actionTitles, 135 | othersHandler: actionHandler 136 | ) 137 | } 138 | 139 | /// The convience version of `presentError:`. 140 | /// Use this func carefully, it maybe iterate many times. 141 | public func doPresentError(_ error: NSError) { 142 | findLastPresentedViewController()?.presentError(error) 143 | } 144 | 145 | /// The convience version of `presentActionSheet(title:message:cancelTitle:cancelHandler:actionTitles:actionHandler:)`. 146 | /// Use this func carefully, it maybe iterate many times. 147 | public func doPresentActionSheet( 148 | title: String = "", 149 | message: String, 150 | cancelTitle: String = "取消", 151 | cancelHandler: ((UIAlertAction) -> ())? = nil, 152 | actionTitles:[String], 153 | actionHandler:((UIAlertAction) -> ())? = nil) 154 | { 155 | findLastPresentedViewController()?.presentActionSheet( 156 | title: title, 157 | message: message, 158 | cancelTitle: cancelTitle, 159 | cancelHandler: cancelHandler, 160 | actionTitles: actionTitles, 161 | actionHandler: actionHandler 162 | ) 163 | } 164 | 165 | // MARK: - Find last time presented view controller 166 | 167 | /// Returns the most recently presented UIViewController (visible). 168 | /// http://stackoverflow.com/questions/24825123/get-the-current-view-controller-from-the-app-delegate 169 | public func findLastPresentedViewController() -> UIViewController? { 170 | func findTopLevelViewController(_ viewController: UIViewController) -> UIViewController? { 171 | if let vc = viewController.presentedViewController { 172 | return findTopLevelViewController(vc) 173 | } else if let vc = viewController as? UISplitViewController { 174 | if let vc = vc.viewControllers.last { 175 | return findTopLevelViewController(vc) 176 | } 177 | return vc 178 | } else if let vc = viewController as? UINavigationController { 179 | if let vc = vc.topViewController { 180 | return findTopLevelViewController(vc) 181 | } 182 | return vc 183 | } else if let vc = viewController as? UITabBarController { 184 | if let vc = vc.selectedViewController { 185 | return findTopLevelViewController(vc) 186 | } 187 | return vc 188 | } else { 189 | return viewController 190 | } 191 | } 192 | 193 | if let rootViewController = UIApplication.shared.keyWindow?.rootViewController { 194 | return findTopLevelViewController(rootViewController) 195 | } 196 | 197 | return nil 198 | } 199 | 200 | // MARK: - Present UIImagePickerController 201 | 202 | private extension UIImagePickerController { 203 | var imagePickerCompletionHandlerWrapper: ClosureDecorator<(UIImagePickerController, UIImage?)> { 204 | get { return associatedObject(forKey: &AssociationKey.imagePickerCompletionHandlerWrapper) as! ClosureDecorator<(UIImagePickerController, UIImage?)> } 205 | set { associate(retainObject: newValue, forKey: &AssociationKey.imagePickerCompletionHandlerWrapper) } 206 | } 207 | } 208 | 209 | public extension UIViewController { 210 | /// Present UIImagePickerController. 211 | public func presentImagePicker(sourceType: UIImagePickerControllerSourceType = .photoLibrary, completionHandler: @escaping ((UIImagePickerController, UIImage?) -> ())) { 212 | let imagePicker = UIImagePickerController() 213 | imagePicker.sourceType = sourceType 214 | imagePicker.videoQuality = .typeLow 215 | imagePicker.delegate = self 216 | imagePicker.allowsEditing = true 217 | imagePicker.view.tintColor = UIApplication.shared.keyWindow?.tintColor 218 | imagePicker.imagePickerCompletionHandlerWrapper = ClosureDecorator(completionHandler) 219 | present(imagePicker, animated: true, completion: nil) 220 | } 221 | } 222 | 223 | extension UIViewController: UINavigationControllerDelegate, UIImagePickerControllerDelegate { 224 | public func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { 225 | picker.presentingViewController?.dismiss(animated: true, completion: nil) 226 | picker.imagePickerCompletionHandlerWrapper.invoke((picker, nil)) 227 | } 228 | 229 | public func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) { 230 | globalThreadAsync { () -> Void in 231 | if let image = info[UIImagePickerControllerEditedImage] as? UIImage { 232 | let newImage = image.orientation(to: .up) 233 | if let imageData = newImage.compress(toByte: 100 * 1024) { 234 | let resultImage = UIImage(data: imageData, scale: UIScreen.main.scale) 235 | mainThreadAsync { 236 | picker.presentingViewController?.dismiss(animated: true, completion: nil) 237 | picker.imagePickerCompletionHandlerWrapper.invoke((picker, resultImage)) 238 | } 239 | return 240 | } 241 | } 242 | } 243 | } 244 | 245 | public func imagePickerController(_ picker: UIImagePickerController, didFinishPickingImage image: UIImage, editingInfo: [String : AnyObject]?) { 246 | picker.presentingViewController?.dismiss(animated: true, completion: nil) 247 | picker.imagePickerCompletionHandlerWrapper.invoke((picker, image)) 248 | } 249 | } 250 | 251 | // MARK: - Navigation 252 | 253 | public extension UIViewController { 254 | public func show(_ viewController: UIViewController) { 255 | navigationController?.show(viewController, sender: self) 256 | } 257 | 258 | public func backToPrevious(animated: Bool = true) { 259 | if let presentingViewController = presentingViewController { 260 | presentingViewController.dismiss(animated: animated, completion: nil) 261 | } else { 262 | _ = navigationController?.popViewController(animated: animated) 263 | } 264 | } 265 | 266 | public func backToRoot(animated: Bool = true) { 267 | if let presentingViewController = presentingViewController { 268 | presentingViewController.dismiss(animated: animated, completion: nil) 269 | } else { 270 | _ = navigationController?.popToRootViewController(animated: animated) 271 | } 272 | } 273 | 274 | public func present(_ viewControllerToPresent: UIViewController, completion: @escaping (() -> ())) { 275 | present(viewControllerToPresent, animated: true, completion: completion) 276 | } 277 | 278 | public func present(_ viewControllerToPresent: UIViewController) { 279 | present(viewControllerToPresent, animated: true, completion: nil) 280 | } 281 | 282 | public func presentTranslucent(_ viewController: UIViewController, modalTransitionStyle: UIModalTransitionStyle = .coverVertical, animated flag: Bool = true, completion: (() -> ())? = nil) { 283 | viewController.modalPresentationStyle = .custom 284 | viewController.modalTransitionStyle = UIDevice.iOS8x ? modalTransitionStyle : .crossDissolve 285 | // Very important 286 | view.window?.rootViewController?.modalPresentationStyle = UIDevice.iOS8x ? .fullScreen : .currentContext 287 | present(viewController, animated: flag, completion: completion) 288 | } 289 | 290 | public func dismiss(completion: (() -> Void)? = nil) { 291 | presentingViewController?.dismiss(animated: true, completion: completion) 292 | } 293 | 294 | public func dismissToTop(animated: Bool = true, completion: (() -> Void)? = nil) { 295 | var presentedViewController = self 296 | while let presentingViewController = presentedViewController.presentingViewController { 297 | presentedViewController = presentingViewController 298 | } 299 | presentedViewController.dismiss(animated: animated, completion: completion) 300 | } 301 | 302 | public func addChild(_ viewController: UIViewController) { 303 | viewController.willMove(toParentViewController: self) 304 | addChildViewController(viewController) 305 | viewController.view.frame = view.frame 306 | view.addSubview(viewController.view) 307 | viewController.didMove(toParentViewController: self) 308 | } 309 | } 310 | 311 | public extension UIViewController { 312 | public func showRightBarButtonItem(withImage image: UIImage?, actionHandler: ((UIBarButtonItem) -> ())?) { 313 | navigationItem.rightBarButtonItem = UIBarButtonItem.make(image: image, actionHandler: actionHandler) 314 | } 315 | 316 | public func showRightBarButtonItem(withTitle title: String, actionHandler: ((UIBarButtonItem) -> ())?) { 317 | navigationItem.rightBarButtonItem = UIBarButtonItem.make(title: title, actionHandler: actionHandler) 318 | } 319 | 320 | public func showRightBarButtonItem(withSystemItem item: UIBarButtonSystemItem, actionHandler: ((UIBarButtonItem) -> ())?) { 321 | navigationItem.rightBarButtonItem = UIBarButtonItem.make(systemItem: item, actionHandler: actionHandler) 322 | } 323 | 324 | public func showLeftBarButtonItem(withImage image: UIImage?, actionHandler: ((UIBarButtonItem) -> ())?) { 325 | navigationItem.leftBarButtonItem = UIBarButtonItem.make(image: image, actionHandler: actionHandler) 326 | } 327 | 328 | public func showLeftBarButtonItem(withTitle title: String, actionHandler: ((UIBarButtonItem) -> ())?) { 329 | navigationItem.leftBarButtonItem = UIBarButtonItem.make(title: title, actionHandler: actionHandler) 330 | } 331 | 332 | public func showLeftBarButtonItem(withSystemItem item: UIBarButtonSystemItem, actionHandler: ((UIBarButtonItem) -> ())?) { 333 | navigationItem.leftBarButtonItem = UIBarButtonItem.make(systemItem: item, actionHandler: actionHandler) 334 | } 335 | } 336 | -------------------------------------------------------------------------------- /Sources/Extension/String+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+Extension.swift 3 | // Copyright (c) 2015-2016 Red Rain (http://mochxiao.com). 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | // THE SOFTWARE. 22 | // 23 | 24 | import UIKit 25 | 26 | // MARK: - URL 27 | /// Stolen from Alamofire 28 | public extension String { 29 | public var escaped: String { 30 | let string: String = self 31 | let generalDelimitersToEncode = ":#[]@" // does not include "?" or "/" due to RFC 3986 - Section 3.4 32 | let subDelimitersToEncode = "!$&'()*+,;=" 33 | 34 | var allowedCharacterSet = CharacterSet.urlQueryAllowed 35 | allowedCharacterSet.remove(charactersIn: "\(generalDelimitersToEncode)\(subDelimitersToEncode)") 36 | 37 | var escaped = "" 38 | 39 | if #available(iOS 8.3, *) { 40 | escaped = string.addingPercentEncoding(withAllowedCharacters: allowedCharacterSet) ?? string 41 | } else { 42 | let batchSize = 50 43 | var index = string.startIndex 44 | 45 | while index != string.endIndex { 46 | let startIndex = index 47 | let endIndex = string.index(index, offsetBy: batchSize, limitedBy: string.endIndex) ?? string.endIndex 48 | let range = startIndex.. Bool { 66 | return NSPredicate(format: "SELF MATCHES %@", regEx).evaluate(with: self) 67 | } 68 | 69 | public var isPhoneNumber: Bool { 70 | return isMatch(regEx: "^(1[345789])\\d{9}") 71 | } 72 | 73 | public var isEmail: Bool { 74 | return isMatch(regEx: "^\\w+([-+.]\\w+)*@\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)*$") 75 | } 76 | } 77 | 78 | // MARK: - 79 | 80 | public extension String { 81 | public var isEmpty: Bool { 82 | return 0 == trimed.count 83 | } 84 | 85 | public var trimed: String { 86 | return trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) 87 | } 88 | 89 | public func layoutSize(font: UIFont, preferredMaxLayoutWidth: CGFloat = UIScreen.width) -> CGSize { 90 | let str = self as NSString 91 | return str.boundingRect( 92 | with: CGSize(width: preferredMaxLayoutWidth, height: CGFloat.greatestFiniteMagnitude), 93 | options: [.usesLineFragmentOrigin, .usesFontLeading, .truncatesLastVisibleLine], 94 | attributes: [.font: font], 95 | context: nil 96 | ).size 97 | } 98 | 99 | public subscript(range: Range) -> String { 100 | return String(self[range]) 101 | } 102 | 103 | public func substring(fromIndex minIndex: Int, toIndex maxIndex: Int) -> String { 104 | let start = index(startIndex, offsetBy: minIndex) 105 | let end = index(startIndex, offsetBy: maxIndex, limitedBy: endIndex) 106 | let range = Range(start ..< end!) 107 | return String(self[range]) 108 | } 109 | 110 | public func substring(fromIndex minIndex: Int) -> String { 111 | let start = index(startIndex, offsetBy: minIndex) 112 | return String(self[start...]) 113 | } 114 | 115 | public func substring(toIndex maxIndex: Int) -> String { 116 | return substring(fromIndex: 0, toIndex: maxIndex) 117 | } 118 | 119 | public static var uniqueIdentifier: String { 120 | return UUID().uuidString.replacingOccurrences(of: "-", with: "") 121 | } 122 | } 123 | 124 | public extension NSAttributedString { 125 | public func layoutSize(preferredMaxLayoutWidth: CGFloat = UIScreen.width) -> CGSize { 126 | return boundingRect( 127 | with: CGSize(width: preferredMaxLayoutWidth, height: CGFloat.greatestFiniteMagnitude), 128 | options: [.usesLineFragmentOrigin, .usesFontLeading, .truncatesLastVisibleLine], 129 | context: nil 130 | ).size 131 | } 132 | } 133 | 134 | // MARK: - String -> Numbers 135 | 136 | public extension String { 137 | /// Convert string to double, shall not input cannot converted string. 138 | public var double: Double { 139 | return Double(self)! 140 | } 141 | 142 | /// Convert string to Float, shall not input cannot converted string. 143 | public var float: Float { 144 | return Float(self)! 145 | } 146 | 147 | /// Convert string to Int, shall not input cannot converted string. 148 | public var int: Int { 149 | return Int(self)! 150 | } 151 | } 152 | 153 | // MARK: - md5 154 | // https://github.com/onevcat/Kingfisher/blob/master/Sources/String%2BMD5.swift 155 | public extension String { 156 | var md5: String { 157 | if let data = self.data(using: .utf8, allowLossyConversion: true) { 158 | 159 | let message = data.withUnsafeBytes { bytes -> [UInt8] in 160 | return Array(UnsafeBufferPointer(start: bytes, count: data.count)) 161 | } 162 | 163 | let MD5Calculator = MD5(message) 164 | let MD5Data = MD5Calculator.calculate() 165 | 166 | let MD5String = NSMutableString() 167 | for c in MD5Data { 168 | MD5String.appendFormat("%02x", c) 169 | } 170 | return MD5String as String 171 | 172 | } else { 173 | return self 174 | } 175 | } 176 | } 177 | 178 | 179 | /** array of bytes, little-endian representation */ 180 | func arrayOfBytes(_ value: T, length: Int? = nil) -> [UInt8] { 181 | let totalBytes = length ?? (MemoryLayout.size * 8) 182 | 183 | let valuePointer = UnsafeMutablePointer.allocate(capacity: 1) 184 | valuePointer.pointee = value 185 | 186 | let bytes = valuePointer.withMemoryRebound(to: UInt8.self, capacity: totalBytes) { (bytesPointer) -> [UInt8] in 187 | var bytes = [UInt8](repeating: 0, count: totalBytes) 188 | for j in 0...size, totalBytes) { 189 | bytes[totalBytes - 1 - j] = (bytesPointer + j).pointee 190 | } 191 | return bytes 192 | } 193 | 194 | valuePointer.deinitialize() 195 | valuePointer.deallocate(capacity: 1) 196 | 197 | return bytes 198 | } 199 | 200 | extension Int { 201 | /** Array of bytes with optional padding (little-endian) */ 202 | func bytes(_ totalBytes: Int = MemoryLayout.size) -> [UInt8] { 203 | return arrayOfBytes(self, length: totalBytes) 204 | } 205 | 206 | } 207 | 208 | extension NSMutableData { 209 | 210 | /** Convenient way to append bytes */ 211 | func appendBytes(_ arrayOfBytes: [UInt8]) { 212 | append(arrayOfBytes, length: arrayOfBytes.count) 213 | } 214 | 215 | } 216 | 217 | protocol HashProtocol { 218 | var message: Array { get } 219 | 220 | /** Common part for hash calculation. Prepare header data. */ 221 | func prepare(_ len: Int) -> Array 222 | } 223 | 224 | extension HashProtocol { 225 | 226 | func prepare(_ len: Int) -> Array { 227 | var tmpMessage = message 228 | 229 | // Step 1. Append Padding Bits 230 | tmpMessage.append(0x80) // append one bit (UInt8 with one bit) to message 231 | 232 | // append "0" bit until message length in bits ≡ 448 (mod 512) 233 | var msgLength = tmpMessage.count 234 | var counter = 0 235 | 236 | while msgLength % len != (len - 8) { 237 | counter += 1 238 | msgLength += 1 239 | } 240 | 241 | tmpMessage += Array(repeating: 0, count: counter) 242 | return tmpMessage 243 | } 244 | } 245 | 246 | func toUInt32Array(_ slice: ArraySlice) -> Array { 247 | var result = Array() 248 | result.reserveCapacity(16) 249 | 250 | for idx in stride(from: slice.startIndex, to: slice.endIndex, by: MemoryLayout.size) { 251 | let d0 = UInt32(slice[idx.advanced(by: 3)]) << 24 252 | let d1 = UInt32(slice[idx.advanced(by: 2)]) << 16 253 | let d2 = UInt32(slice[idx.advanced(by: 1)]) << 8 254 | let d3 = UInt32(slice[idx]) 255 | let val: UInt32 = d0 | d1 | d2 | d3 256 | 257 | result.append(val) 258 | } 259 | return result 260 | } 261 | 262 | struct BytesIterator: IteratorProtocol { 263 | 264 | let chunkSize: Int 265 | let data: [UInt8] 266 | 267 | init(chunkSize: Int, data: [UInt8]) { 268 | self.chunkSize = chunkSize 269 | self.data = data 270 | } 271 | 272 | var offset = 0 273 | 274 | mutating func next() -> ArraySlice? { 275 | let end = min(chunkSize, data.count - offset) 276 | let result = data[offset.. 0 ? result : nil 279 | } 280 | } 281 | 282 | struct BytesSequence: Sequence { 283 | let chunkSize: Int 284 | let data: [UInt8] 285 | 286 | func makeIterator() -> BytesIterator { 287 | return BytesIterator(chunkSize: chunkSize, data: data) 288 | } 289 | } 290 | 291 | func rotateLeft(_ value: UInt32, bits: UInt32) -> UInt32 { 292 | return ((value << bits) & 0xFFFFFFFF) | (value >> (32 - bits)) 293 | } 294 | 295 | class MD5: HashProtocol { 296 | 297 | static let size = 16 // 128 / 8 298 | let message: [UInt8] 299 | 300 | init (_ message: [UInt8]) { 301 | self.message = message 302 | } 303 | 304 | /** specifies the per-round shift amounts */ 305 | private let shifts: [UInt32] = [7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 306 | 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 307 | 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 308 | 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21] 309 | 310 | /** binary integer part of the sines of integers (Radians) */ 311 | private let sines: [UInt32] = [0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee, 312 | 0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501, 313 | 0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be, 314 | 0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821, 315 | 0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa, 316 | 0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8, 317 | 0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed, 318 | 0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a, 319 | 0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c, 320 | 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70, 321 | 0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x4881d05, 322 | 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665, 323 | 0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039, 324 | 0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1, 325 | 0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1, 326 | 0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391] 327 | 328 | private let hashes: [UInt32] = [0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476] 329 | 330 | func calculate() -> [UInt8] { 331 | var tmpMessage = prepare(64) 332 | tmpMessage.reserveCapacity(tmpMessage.count + 4) 333 | 334 | // hash values 335 | var hh = hashes 336 | 337 | // Step 2. Append Length a 64-bit representation of lengthInBits 338 | let lengthInBits = (message.count * 8) 339 | let lengthBytes = lengthInBits.bytes(64 / 8) 340 | tmpMessage += lengthBytes.reversed() 341 | 342 | // Process the message in successive 512-bit chunks: 343 | let chunkSizeBytes = 512 / 8 // 64 344 | 345 | for chunk in BytesSequence(chunkSize: chunkSizeBytes, data: tmpMessage) { 346 | // break chunk into sixteen 32-bit words M[j], 0 ≤ j ≤ 15 347 | var M = toUInt32Array(chunk) 348 | assert(M.count == 16, "Invalid array") 349 | 350 | // Initialize hash value for this chunk: 351 | var A: UInt32 = hh[0] 352 | var B: UInt32 = hh[1] 353 | var C: UInt32 = hh[2] 354 | var D: UInt32 = hh[3] 355 | 356 | var dTemp: UInt32 = 0 357 | 358 | // Main loop 359 | for j in 0 ..< sines.count { 360 | var g = 0 361 | var F: UInt32 = 0 362 | 363 | switch j { 364 | case 0...15: 365 | F = (B & C) | ((~B) & D) 366 | g = j 367 | break 368 | case 16...31: 369 | F = (D & B) | (~D & C) 370 | g = (5 * j + 1) % 16 371 | break 372 | case 32...47: 373 | F = B ^ C ^ D 374 | g = (3 * j + 5) % 16 375 | break 376 | case 48...63: 377 | F = C ^ (B | (~D)) 378 | g = (7 * j) % 16 379 | break 380 | default: 381 | break 382 | } 383 | dTemp = D 384 | D = C 385 | C = B 386 | B = B &+ rotateLeft((A &+ F &+ sines[j] &+ M[g]), bits: shifts[j]) 387 | A = dTemp 388 | } 389 | 390 | hh[0] = hh[0] &+ A 391 | hh[1] = hh[1] &+ B 392 | hh[2] = hh[2] &+ C 393 | hh[3] = hh[3] &+ D 394 | } 395 | 396 | var result = [UInt8]() 397 | result.reserveCapacity(hh.count / 4) 398 | 399 | hh.forEach { 400 | let itemLE = $0.littleEndian 401 | result += [UInt8(itemLE & 0xff), UInt8((itemLE >> 8) & 0xff), UInt8((itemLE >> 16) & 0xff), UInt8((itemLE >> 24) & 0xff)] 402 | } 403 | return result 404 | } 405 | } 406 | -------------------------------------------------------------------------------- /Sources/Extension/UIImage+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImage+Extension.swift 3 | // Copyright (c) 2015-2016 Red Rain (http://mochxiao.com). 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | // THE SOFTWARE. 22 | // 23 | 24 | import UIKit 25 | 26 | // MARK: - Load image 27 | 28 | public extension UIImage { 29 | /// Load bundle image with file name 30 | public class func load(fileName: String, extensionType: String = "png") -> UIImage? { 31 | func pathForResource(_ fileName: String, ofType: String) -> String? { 32 | return Bundle.main.path(forResource: fileName, ofType: ofType) 33 | } 34 | 35 | if UIScreen.main.bounds.width > 375.0 { 36 | if let filePath = pathForResource("\(fileName)@3x", ofType: extensionType) { 37 | return UIImage(contentsOfFile: filePath) 38 | } 39 | } 40 | 41 | if let filePath = pathForResource("\(fileName)@2x", ofType: extensionType) { 42 | return UIImage(contentsOfFile: filePath) 43 | } 44 | 45 | if let filePath = pathForResource(fileName, ofType: extensionType) { 46 | return UIImage(contentsOfFile: filePath) 47 | } 48 | 49 | return nil 50 | } 51 | } 52 | 53 | // MARK: - Compress & Decompress 54 | 55 | public extension UIImage { 56 | /// Represent current image to render mode original. 57 | public var original: UIImage { 58 | return withRenderingMode(.alwaysOriginal) 59 | } 60 | 61 | /// Decompressed image. 62 | public var decompressed: UIImage? { 63 | UIGraphicsBeginImageContextWithOptions(size, true, 0) 64 | draw(at: CGPoint.zero) 65 | guard let decompressedImage = UIGraphicsGetImageFromCurrentImageContext() else { 66 | return nil 67 | } 68 | UIGraphicsEndImageContext() 69 | return decompressedImage 70 | } 71 | 72 | /// Compress image quality as possible to fit target size. 73 | public func compressQuality(toByte maxLength: Int) -> Data? { 74 | let image: UIImage = self 75 | var compression: CGFloat = 1 76 | if let data = UIImageJPEGRepresentation(image, compression), data.count < maxLength { 77 | return data 78 | } 79 | 80 | // Compress by quality 81 | var max: CGFloat = 1 82 | var min: CGFloat = 0 83 | var data: Data! 84 | for _ in 0 ..< 6 { 85 | compression = (max + min) / 2 86 | data = UIImageJPEGRepresentation(image, compression) 87 | if nil != data { 88 | if CGFloat(data.count) < CGFloat(maxLength) * 0.9 { 89 | min = compression 90 | } else if data.count > maxLength { 91 | max = compression 92 | } else { 93 | break 94 | } 95 | } 96 | } 97 | 98 | return data 99 | } 100 | 101 | /// Compress image quality & size as possible to fit target size. 102 | func compress(toByte maxLength: Int) -> Data? { 103 | let image: UIImage = self 104 | var compression: CGFloat = 1 105 | if let data = UIImageJPEGRepresentation(image, compression), data.count < maxLength { 106 | return data 107 | } 108 | 109 | // Compress by quality 110 | var max: CGFloat = 1 111 | var min: CGFloat = 0 112 | var data: Data! 113 | for _ in 0 ..< 6 { 114 | compression = (max + min) / 2 115 | data = UIImageJPEGRepresentation(image, compression) 116 | if nil != data { 117 | if CGFloat(data.count) < CGFloat(maxLength) * 0.9 { 118 | min = compression 119 | } else if data.count > maxLength { 120 | max = compression 121 | } else { 122 | break 123 | } 124 | } 125 | } 126 | if data != nil && data.count < maxLength { 127 | return data 128 | } 129 | var resultImage: UIImage! = UIImage(data: data) 130 | 131 | // Compress by size 132 | var lastDataLength: Int = 0 133 | while resultImage != nil && data.count > maxLength, data.count != lastDataLength { 134 | lastDataLength = data.count 135 | let ratio: CGFloat = CGFloat(maxLength) / CGFloat(data.count) 136 | let size: CGSize = CGSize(width: Int(resultImage.size.width * sqrt(ratio)), 137 | height: Int(resultImage.size.height * sqrt(ratio))) 138 | UIGraphicsBeginImageContext(size) 139 | resultImage.draw(in: CGRect(x: 0, y: 0, width: size.width, height: size.height)) 140 | resultImage = UIGraphicsGetImageFromCurrentImageContext()! 141 | UIGraphicsEndImageContext() 142 | data = UIImageJPEGRepresentation(resultImage, compression)! 143 | } 144 | return data 145 | } 146 | 147 | public func orientation(to orientation: UIImageOrientation) -> UIImage { 148 | if imageOrientation == orientation { 149 | return self 150 | } 151 | 152 | if let CGImage = cgImage { 153 | return UIImage(cgImage: CGImage, scale: UIScreen.main.scale, orientation: orientation) 154 | } 155 | logging("Cannot complete action.") 156 | return self 157 | } 158 | 159 | public var bytes: Data? { 160 | // Establish color space 161 | let colorSpace = CGColorSpaceCreateDeviceRGB() 162 | 163 | // Establish context 164 | let width: Int = Int(size.width) 165 | let height: Int = Int(size.height) 166 | guard let context = CGContext( 167 | data: nil, 168 | width: width, 169 | height: height, 170 | bitsPerComponent: 8, 171 | bytesPerRow: width * 4, 172 | space: colorSpace, 173 | bitmapInfo: CGImageAlphaInfo.premultipliedFirst.rawValue 174 | ) else { return nil } 175 | 176 | // Draw source into context bytes 177 | let rect = CGRectMake(size: size) 178 | guard let CGImage = cgImage else { 179 | return nil 180 | } 181 | context.draw(CGImage, in: rect) 182 | 183 | // Create NSData from bytes 184 | if let data = context.data { 185 | return Data(bytes: UnsafeMutableRawPointer(data), count: (width * height * 4)) 186 | } else { 187 | return nil 188 | } 189 | } 190 | 191 | public func color(atPixel point: CGPoint) -> UIColor? { 192 | let width = size.width 193 | let height = size.height 194 | if !CGRect(x: 0, y: 0, width: width, height: height).contains(point) { 195 | return nil 196 | } 197 | 198 | let bytesPerPixel = 4 199 | let bytesPerRow = bytesPerPixel * 1 200 | let colorSpace = CGColorSpaceCreateDeviceRGB() 201 | let bitsPerComponent = 8 202 | var pixelData: [CGFloat] = [0, 0, 0, 0] 203 | 204 | guard let context = CGContext( 205 | data: &pixelData, 206 | width: 1, 207 | height: 1, 208 | bitsPerComponent: bitsPerComponent, 209 | bytesPerRow: bytesPerRow, 210 | space: colorSpace, 211 | bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue 212 | ) else { return nil } 213 | 214 | let pointX = trunc(point.x) 215 | let pointY = trunc(point.y) 216 | guard let cgImage = cgImage else { return nil } 217 | context.translateBy(x: -pointX, y: pointY - height) 218 | context.draw(cgImage, in: CGRect(x: 0, y: 0, width: width, height: height)) 219 | return UIColor(red: pixelData[0] / 255.0, green: pixelData[1] / 255.0, blue: pixelData[2] / 255.0, alpha: pixelData[3] / 255.0) 220 | } 221 | } 222 | 223 | // MARK: - Draw 224 | 225 | public extension UIImage { 226 | public class func make( 227 | color: UIColor, 228 | size: CGSize = CGSize(width: 1, height: 1), 229 | roundingCorners: UIRectCorner = .allCorners, 230 | radius: CGFloat = 0, 231 | strokeColor: UIColor = UIColor.clear, 232 | strokeLineWidth: CGFloat = 0) -> UIImage? 233 | { 234 | let rect = CGRect(origin: CGPoint(x: 0, y: 0), size: size) 235 | UIGraphicsBeginImageContextWithOptions(rect.size, false, 0) 236 | 237 | guard let context = UIGraphicsGetCurrentContext() else { fatalError() } 238 | 239 | context.setFillColor(color.cgColor) 240 | context.setLineWidth(strokeLineWidth) 241 | context.setStrokeColor(strokeColor.cgColor) 242 | 243 | let roundedRect = CGRect( 244 | x: strokeLineWidth, 245 | y: strokeLineWidth, 246 | width: rect.width - strokeLineWidth * 2, 247 | height: rect.height - strokeLineWidth * 2) 248 | let path = UIBezierPath( 249 | roundedRect: roundedRect, 250 | byRoundingCorners: roundingCorners, 251 | cornerRadii: CGSize(width: radius, height: radius) 252 | ) 253 | context.addPath(path.cgPath) 254 | 255 | context.drawPath(using: .fillStroke) 256 | 257 | guard let output = UIGraphicsGetImageFromCurrentImageContext() else { 258 | return nil 259 | } 260 | UIGraphicsEndImageContext() 261 | return output 262 | } 263 | 264 | public func remake( 265 | roundingCorners corners: UIRectCorner = .allCorners, 266 | radius: CGFloat = 0, 267 | strokeColor: UIColor? = nil, 268 | strokeLineWidth: CGFloat = 0, 269 | stockLineJoin: CGLineJoin = .miter) -> UIImage 270 | { 271 | UIGraphicsBeginImageContextWithOptions(size, false, 0) 272 | defer { UIGraphicsEndImageContext() } 273 | guard let context = UIGraphicsGetCurrentContext() else { 274 | return self 275 | } 276 | context.scaleBy(x: 1, y: -1) 277 | context.translateBy(x: 0, y: -size.height) 278 | 279 | let roundedRect = CGRect(origin: CGPoint(x: 0, y: 0), size: size) 280 | let sideLength = min(roundedRect.size.width, roundedRect.size.height) 281 | if strokeLineWidth < sideLength * 0.5 { 282 | let roundedpath = UIBezierPath( 283 | roundedRect: roundedRect.insetBy(dx: strokeLineWidth, dy: strokeLineWidth), 284 | byRoundingCorners: corners, 285 | cornerRadii: CGSize(width: radius, height: strokeLineWidth) 286 | ) 287 | roundedpath.close() 288 | 289 | context.saveGState() 290 | context.addPath(roundedpath.cgPath) 291 | context.clip() 292 | context.draw(cgImage!, in: roundedRect) 293 | context.restoreGState() 294 | } 295 | 296 | if nil != strokeColor && strokeLineWidth > 0 { 297 | let strokeInset = (floor(strokeLineWidth * scale) + 0.5) / scale 298 | let strokeRect = roundedRect.insetBy(dx: strokeInset, dy: strokeInset) 299 | let strokeRadius = radius > scale / 2.0 ? radius - scale / 2.0 : 0.0 300 | let strokePath = UIBezierPath( 301 | roundedRect: strokeRect, 302 | byRoundingCorners: corners, 303 | cornerRadii: CGSize(width: strokeRadius, height: strokeLineWidth) 304 | ) 305 | strokePath.close() 306 | 307 | context.saveGState() 308 | context.setStrokeColor(strokeColor!.cgColor) 309 | context.setLineWidth(strokeLineWidth) 310 | context.setLineJoin(stockLineJoin) 311 | context.addPath(strokePath.cgPath) 312 | context.strokePath() 313 | context.restoreGState() 314 | } 315 | 316 | if let output = UIGraphicsGetImageFromCurrentImageContext() { 317 | return output 318 | } 319 | return self 320 | } 321 | 322 | public var circle: UIImage { 323 | var newImage: UIImage = self 324 | let sideLength = min(size.width, size.height) 325 | if size.width != size.height { 326 | let center = CGPoint(x: size.width * 0.5, y: size.height * 0.5) 327 | let newRect = CGRect(x: center.x - sideLength * 0.5, y: center.y - sideLength * 0.5, width: sideLength, height: sideLength) 328 | if let image = extracting(in: newRect) { 329 | newImage = image 330 | } 331 | } 332 | return newImage.remake(radius: sideLength * 0.5) 333 | } 334 | 335 | public func remake(alpha: CGFloat) -> UIImage { 336 | UIGraphicsBeginImageContext(size) 337 | defer { UIGraphicsEndImageContext() } 338 | 339 | let rect = CGRect(origin: CGPoint(x: 0, y: 0), size: size) 340 | draw(in: rect, blendMode: .normal, alpha: alpha) 341 | if let output = UIGraphicsGetImageFromCurrentImageContext() { 342 | return output 343 | } 344 | return self 345 | } 346 | 347 | public func rendering(color: UIColor, alpha: CGFloat = 1.0) -> UIImage { 348 | UIGraphicsBeginImageContext(size) 349 | defer { UIGraphicsEndImageContext() } 350 | 351 | color.setFill() 352 | let rect = CGRect(x: 0, y: 0, width: size.width, height: size.height) 353 | UIRectFill(rect) 354 | draw(in: rect, blendMode: .overlay, alpha: alpha) 355 | if let output = UIGraphicsGetImageFromCurrentImageContext() { 356 | return output 357 | } 358 | return self 359 | } 360 | 361 | public func builtThumbnail(targetSize: CGSize, useFitting: Bool = true) -> UIImage { 362 | UIGraphicsBeginImageContextWithOptions(targetSize, false, 0.0) 363 | defer { UIGraphicsEndImageContext() } 364 | 365 | // Establish the output thumbnail rectangle 366 | let targetRect = CGRectMake(origin: CGPoint.zero, size: targetSize) 367 | // Create the source image’s bounding rectangle 368 | let naturalRect = CGRectMake(origin: CGPoint.zero, size: size) 369 | // Calculate fitting or filling destination rectangle 370 | // See Chapter 2 for a discussion on these functions 371 | let destinationRect = useFitting ? naturalRect.fitting(in: targetRect) : naturalRect.filling(in: targetRect) 372 | // Draw the new thumbnail 373 | draw(in: destinationRect) 374 | // Retrieve and return the new image 375 | if let output = UIGraphicsGetImageFromCurrentImageContext() { 376 | return output 377 | } 378 | return self 379 | } 380 | 381 | /// Extract image 382 | public func extracting(in subRect: CGRect) -> UIImage? { 383 | if let imageRef = cgImage!.cropping(to: subRect) { 384 | return UIImage(cgImage: imageRef) 385 | } 386 | return nil 387 | } 388 | 389 | /// Watermarking 390 | public func watermarking( 391 | text: String, 392 | font: UIFont, 393 | color: UIColor = UIColor.white, 394 | rotate: Double = Double.pi / 4.0 ) -> UIImage 395 | { 396 | UIGraphicsBeginImageContextWithOptions(size, false, 0) 397 | defer { UIGraphicsEndImageContext() } 398 | 399 | let context = UIGraphicsGetCurrentContext() 400 | 401 | // Draw the original image into the context 402 | let targetRect = CGRectMake(size: size) 403 | draw(in: targetRect) 404 | 405 | // Rotate the context 406 | let center = targetRect.center 407 | context!.translateBy(x: center.x, y: center.y) 408 | context!.rotate(by: CGFloat(rotate)) 409 | context!.translateBy(x: -center.x, y: -center.y) 410 | 411 | let stringSize = text.layoutSize(font: font, preferredMaxLayoutWidth: size.width) 412 | let stringRect = CGRectMake(size: stringSize).centering(in: CGRectMake(size: size)) 413 | 414 | // Draw the string, using a blend mode 415 | context!.setBlendMode(.normal) 416 | (text as NSString).draw( 417 | in: stringRect, 418 | withAttributes: [NSAttributedStringKey.foregroundColor: color] 419 | ) 420 | 421 | // Retrieve the new image 422 | if let output = UIGraphicsGetImageFromCurrentImageContext() { 423 | return output 424 | } 425 | return self 426 | } 427 | } 428 | 429 | public func UIImageFrom( 430 | color: UIColor, 431 | size: CGSize = CGSize(width: 1, height: 1), 432 | roundingCorners: UIRectCorner = .allCorners, 433 | radius: CGFloat = 0, 434 | strokeColor: UIColor = UIColor.clear, 435 | strokeLineWidth: CGFloat = 0) -> UIImage? 436 | { 437 | return UIImage.make( 438 | color: color, 439 | size: size, 440 | roundingCorners: roundingCorners, 441 | radius: radius, 442 | strokeColor: strokeColor, 443 | strokeLineWidth: strokeLineWidth 444 | ) 445 | } 446 | 447 | public extension UIImage { 448 | @available(iOS 8.0, *) 449 | public var isQRCode: Bool { 450 | if let CIImage = CIImage(image: self) { 451 | let detector = CIDetector( 452 | ofType: CIDetectorTypeQRCode, context: nil, 453 | options: [CIDetectorAccuracy : CIDetectorAccuracyHigh] 454 | ) 455 | let features = detector!.features(in: CIImage) 456 | if let first = features.first as? CIQRCodeFeature { 457 | return first.messageString!.count > 0 458 | } 459 | } 460 | return false 461 | } 462 | } 463 | -------------------------------------------------------------------------------- /Sources/Extension/CoreGraphics+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CoreGraphics+Extension.swift 3 | // Copyright (c) 2015-2016 Red Rain (http://mochxiao.com). 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | // THE SOFTWARE. 22 | // 23 | 24 | import Foundation 25 | import CoreGraphics 26 | import UIKit 27 | 28 | // MARK: - 29 | 30 | public extension Double { 31 | public var radian: CGFloat { 32 | return CGFloat(self / 180.0 * Double.pi) 33 | } 34 | 35 | public var angle: CGFloat { 36 | return CGFloat(self / Double.pi * 180.0) 37 | } 38 | 39 | public var pointToPixel: CGFloat { 40 | return CGFloat(self) * UIScreen.main.scale 41 | } 42 | 43 | public var pixelToPoint: CGFloat { 44 | return CGFloat(self) / UIScreen.main.scale 45 | } 46 | } 47 | 48 | public extension CGFloat { 49 | public var radian: CGFloat { 50 | return CGFloat(Double(native / 180.0) * Double.pi) 51 | } 52 | 53 | public var angle: CGFloat { 54 | return CGFloat(Double(native) / Double.pi * 180.0) 55 | } 56 | 57 | public var pointToPixel: CGFloat { 58 | return self * UIScreen.main.scale 59 | } 60 | 61 | public var pixelToPoint: CGFloat { 62 | return self / UIScreen.main.scale 63 | } 64 | } 65 | 66 | public extension CGFloat { 67 | public var ceilling: CGFloat { return ceil(self) } 68 | public var flooring: CGFloat { return floor(self) } 69 | } 70 | 71 | public extension CGPoint { 72 | public var ceilling: CGPoint { return CGPoint(x: ceil(x), y: ceil(y)) } 73 | public var flooring: CGPoint { return CGPoint(x: floor(x), y: floor(y)) } 74 | 75 | public func makeVector(to other: CGPoint) -> CGVector { 76 | return CGVector(dx: other.x - x, dy: y - other.y) 77 | } 78 | } 79 | 80 | public let CGPointZert = CGPoint.zero 81 | 82 | public func CGPointMake(_ x: CGFloat, _ y: CGFloat) -> CGPoint { 83 | return CGPoint(x: x, y: y) 84 | } 85 | 86 | public func CGPointMake(_ x: Double, _ y: Double) -> CGPoint { 87 | return CGPoint(x: x, y: y) 88 | } 89 | 90 | public func CGPointMake(_ x: Int, _ y: Int) -> CGPoint { 91 | return CGPoint(x: x, y: y) 92 | } 93 | 94 | // MARK: - 95 | 96 | public extension CGSize { 97 | public var ceilling: CGSize { return CGSize(width: ceil(width), height: ceil(height)) } 98 | public var flooring: CGSize { return CGSize(width: floor(width), height: floor(height)) } 99 | 100 | /// Multiply the size components by the factor 101 | public func scale(factor: CGFloat) -> CGSize { 102 | return CGSize(width: width * factor, height: height * factor) 103 | } 104 | 105 | /// Calculate scale for fitting a size to a destination size 106 | public func scale(aspectToFit size: CGSize) -> CGSize { 107 | return scale(factor: min(size.width / width, size.height / height)) 108 | } 109 | 110 | // Calculate scale for filling a destination size 111 | public func scale(aspectToFill size: CGSize) -> CGSize { 112 | return scale(factor: max(size.width / width, size.height / height)) 113 | } 114 | } 115 | 116 | public let CGSizeZert = CGSize.zero 117 | 118 | public func CGSizeMake(_ width: CGFloat, _ height: CGFloat) -> CGSize { 119 | return CGSize(width: width, height: height) 120 | } 121 | 122 | public func CGSizeMake(_ width: Double, _ height: Double) -> CGSize { 123 | return CGSize(width: width, height: height) 124 | } 125 | 126 | public func CGSizeMake(_ width: Int, _ height: Int) -> CGSize { 127 | return CGSize(width: width, height: height) 128 | } 129 | 130 | public func CGSizeEqualToSize(_ lhs: CGSize, _ rhs: CGSize) -> Bool { 131 | return lhs.equalTo(rhs) 132 | } 133 | 134 | // MARK: - 135 | 136 | public extension CGVector { 137 | public var ceilling: CGVector { return CGVector(dx: ceil(dx), dy: ceil(dy)) } 138 | public var flooring: CGVector { return CGVector(dx: floor(dx), dy: floor(dy)) } 139 | } 140 | 141 | public func CGVectorMake(_ dx: CGFloat, dy: CGFloat) -> CGVector { 142 | return CGVector(dx: dx, dy: dy) 143 | } 144 | 145 | public func CGVectorMake(_ dx: Double, dy: Double) -> CGVector { 146 | return CGVector(dx: dx, dy: dy) 147 | } 148 | 149 | public func CGVectorMake(_ dx: Int, dy: Int) -> CGVector { 150 | return CGVector(dx: dx, dy: dy) 151 | } 152 | 153 | // MARK: - 154 | 155 | public extension CGRect { 156 | public var ceilling: CGRect { return CGRectMake(size: size.ceilling) } 157 | public var flooring: CGRect { return CGRectMake(size: size.flooring) } 158 | 159 | /// Return a rect centered a source to a destination 160 | public func centering(in destination: CGRect) -> CGRect { 161 | let dx: CGFloat = destination.midX - midX 162 | let dy: CGFloat = destination.midY - midY 163 | return offsetBy(dx: dx, dy: dy) 164 | } 165 | 166 | /// Return a rect fitting a source to a destination 167 | public func fitting(in destination: CGRect) -> CGRect { 168 | let targetSize = size.scale(aspectToFit: destination.size) 169 | return CGRectMake(center: destination.center, size: targetSize) 170 | } 171 | 172 | /// Return a rect that fills the destination 173 | public func filling(in destination: CGRect) -> CGRect { 174 | let targetSize = size.scale(aspectToFill: destination.size) 175 | return CGRectMake(center: destination.center, size: targetSize) 176 | } 177 | 178 | public var left: CGFloat { 179 | set { 180 | var newOrigin = origin 181 | newOrigin.x = newValue 182 | origin = newOrigin 183 | } 184 | get { return origin.x } 185 | } 186 | 187 | public var centerX: CGFloat { 188 | set { 189 | let diff = newValue - midX 190 | var newOrigin = origin 191 | newOrigin.x += diff 192 | origin = newOrigin 193 | } 194 | get { return origin.x + size.width / 2.0 } 195 | } 196 | 197 | public var right: CGFloat { 198 | set { 199 | let diff = newValue - maxX 200 | var newOrigin = origin 201 | newOrigin.x += diff 202 | origin = newOrigin 203 | } 204 | get { return origin.x + size.width } 205 | } 206 | 207 | public var top: CGFloat { 208 | set { 209 | var newOrigin = origin 210 | newOrigin.y = newValue 211 | origin = newOrigin 212 | } 213 | get { return origin.y } 214 | } 215 | 216 | public var centerY: CGFloat { 217 | set { 218 | let diff = newValue - midY 219 | var newOrigin = origin 220 | newOrigin.y += diff 221 | origin = newOrigin 222 | } 223 | get { return origin.y + size.height / 2.0 } 224 | } 225 | 226 | public var bottom: CGFloat { 227 | set { 228 | let diff = newValue - maxY 229 | var newOrigin = origin 230 | newOrigin.y += diff 231 | origin = newOrigin 232 | } 233 | get { return origin.y + size.height } 234 | } 235 | 236 | public var center: CGPoint { 237 | set { 238 | var frame = self 239 | frame.origin.x = newValue.x - frame.size.width * 0.5 240 | frame.origin.y = newValue.y - frame.size.height * 0.5 241 | self = frame 242 | } 243 | get { return CGPoint(x: origin.x + size.width * 0.5, y: origin.y + size.height * 0.5) } 244 | } 245 | 246 | public var lengthX: CGFloat { 247 | set { 248 | var newSize = size 249 | newSize.width = newValue 250 | size = newSize 251 | } 252 | get { return size.width } 253 | } 254 | 255 | public var lengthY: CGFloat { 256 | set { 257 | var newSize = size 258 | newSize.height = newValue 259 | size = newSize 260 | } 261 | get { return size.height } 262 | } 263 | } 264 | 265 | public let CGRectZero = CGRect.zero 266 | public let CGRectNull = CGRect.null 267 | public let CGRectInfinite = CGRect.infinite 268 | 269 | /// Return center for rect 270 | public func CGRectGetCenter(_ rect: CGRect) -> CGPoint { 271 | return rect.center 272 | } 273 | 274 | public func CGRectMake(origin: CGPoint = CGPoint.zero, size: CGSize) -> CGRect { 275 | return CGRect(x: origin.x, y: origin.y, width: size.width, height: size.height) 276 | } 277 | 278 | public func CGRectMake(center: CGPoint, size: CGSize) -> CGRect { 279 | let halfWidth = size.width / 2.0 280 | let halfHeight = size.height / 2.0 281 | return CGRect( 282 | x: center.x - halfWidth, 283 | y: center.y - halfHeight, 284 | width: size.width, 285 | height: size.height 286 | ) 287 | } 288 | 289 | public func CGRectMake(_ x: CGFloat, _ y: CGFloat, _ width: CGFloat, _ height: CGFloat) -> CGRect { 290 | return CGRect(x: x, y: y, width: width, height: height) 291 | } 292 | 293 | public func CGRectMake(_ x: Double, _ y: Double, _ width: Double, _ height: Double) -> CGRect { 294 | return CGRect(x: x, y: y, width: width, height: height) 295 | } 296 | 297 | public func CGRectMake(_ x: Int, _ y: Int, _ width: Int, _ height: Int) -> CGRect { 298 | return CGRect(x: x, y: y, width: width, height: height) 299 | } 300 | 301 | public func CGRectGetMinX(_ rect: CGRect) -> CGFloat { 302 | return rect.minX 303 | } 304 | 305 | public func CGRectGetMidX(_ rect: CGRect) -> CGFloat { 306 | return rect.midX 307 | } 308 | 309 | public func CGRectGetMaxX(_ rect: CGRect) -> CGFloat { 310 | return rect.maxX 311 | } 312 | 313 | public func CGRectGetMinY(_ rect: CGRect) -> CGFloat { 314 | return rect.minY 315 | } 316 | 317 | public func CGRectGetMidY(_ rect: CGRect) -> CGFloat { 318 | return rect.midY 319 | } 320 | 321 | public func CGRectGetMaxY(_ rect: CGRect) -> CGFloat { 322 | return rect.maxY 323 | } 324 | 325 | public func CGRectGetWidth(_ rect: CGRect) -> CGFloat { 326 | return rect.width 327 | } 328 | 329 | public func CGRectGetHeight(_ rect: CGRect) -> CGFloat { 330 | return rect.height 331 | } 332 | 333 | public func CGRectEqualToRect(_ lhs: CGRect, _ rhs: CGRect) -> Bool { 334 | return lhs.equalTo(rhs) 335 | } 336 | 337 | public func CGRectStandardize(_ rect: CGRect) -> CGRect { 338 | return CGRect(x: abs(rect.minX), y: abs(rect.minY), width: abs(rect.width), height: abs(rect.height)) 339 | } 340 | 341 | public func CGRectIsEmpty(_ rect: CGRect) -> Bool { 342 | return rect.isEmpty 343 | } 344 | 345 | public func CGRectIsNull(_ rect: CGRect) -> Bool { 346 | return rect.isNull 347 | } 348 | 349 | public func CGRectIsInfinite(_ rect: CGRect) -> Bool { 350 | return rect.isInfinite 351 | } 352 | 353 | public func CGRectInset(_ rect: CGRect, _ dx: CGFloat, _ dy: CGFloat) -> CGRect { 354 | return rect.insetBy(dx: dx, dy: dy) 355 | } 356 | 357 | public func CGRectOffset(_ rect: CGRect, _ dx: CGFloat, _ dy: CGFloat) -> CGRect { 358 | return rect.offsetBy(dx: dx, dy: dy) 359 | } 360 | 361 | public func CGRectIntegral(_ rect: CGRect) -> CGRect { 362 | return rect.integral 363 | } 364 | 365 | public func CGRectUnion(_ rect1: CGRect, _ rect2: CGRect) -> CGRect { 366 | return rect1.union(rect2) 367 | } 368 | 369 | public func CGRectIntersection(_ rect1: CGRect, _ rect2: CGRect) -> CGRect { 370 | return rect2.intersection(rect2) 371 | } 372 | 373 | public func CGRectDivide(_ rect: CGRect, _ atDistance: CGFloat, _ from: CGRectEdge) -> (slice: CGRect, remainder: CGRect) { 374 | return rect.divided(atDistance: atDistance, from: from) 375 | } 376 | 377 | public func CGRectContainsPoint(_ rect: CGRect, _ point: CGPoint) -> Bool { 378 | return rect.contains(point) 379 | } 380 | 381 | public func CGRectContainsRect(_ rect1: CGRect, _ rect2: CGRect) -> Bool { 382 | return rect1.contains(rect2) 383 | } 384 | 385 | public func CGRectIntersectsRect(_ rect1: CGRect, _ rect2: CGRect) -> Bool { 386 | return rect1.intersects(rect2) 387 | } 388 | 389 | // MARK: - 390 | 391 | public extension CGAffineTransform { 392 | /// X scale from transform 393 | public var xScale: CGFloat { return sqrt(a * a + c * c) } 394 | 395 | /// Y scale from transform 396 | public var yScale: CGFloat { return sqrt(b * b + d * d) } 397 | 398 | /// Rotation in radians 399 | public var rotation: CGFloat { return CGFloat(atan2f(Float(b), Float(a))) } 400 | } 401 | 402 | // MARK: - 403 | 404 | public extension UIBezierPath { 405 | /// The path bounding box of `path'. 406 | public var boundingBox: CGRect { 407 | return cgPath.boundingBoxOfPath 408 | } 409 | 410 | /// The calculated bounds taking line width into account 411 | public var boundingWithLineBox: CGRect { 412 | return boundingBox.insetBy(dx: -lineWidth / 2.0, dy: -lineWidth / 2.0) 413 | } 414 | 415 | /// The center point for the path bounding box of `path'. 416 | public var boundingCenter: CGPoint { 417 | return CGRectGetCenter(boundingBox) 418 | } 419 | 420 | /// Translate path’s origin to its center before applying the transform 421 | public func centering(transform: CGAffineTransform) { 422 | let center = boundingCenter 423 | var t = CGAffineTransform.identity 424 | t = t.translatedBy(x: center.x, y: center.y) 425 | t = transform.concatenating(t) 426 | t = t.translatedBy(x: -center.x, y: -center.y) 427 | apply(t) 428 | } 429 | 430 | public func offset(vector: CGVector) { 431 | centering(transform: CGAffineTransform(translationX: vector.dx, y: vector.dy)) 432 | } 433 | 434 | public func rotate(theta: CGFloat) { 435 | centering(transform: CGAffineTransform(rotationAngle: theta)) 436 | } 437 | 438 | /// Move to a new center 439 | public func moveCenter(to position: CGPoint) { 440 | let bounds = boundingBox 441 | var vector = bounds.origin.makeVector(to: position) 442 | vector.dx -= bounds.size.width / 2.0 443 | vector.dy -= bounds.size.height / 2.0 444 | offset(vector: vector) 445 | } 446 | 447 | public func scale(xFactor: CGFloat, yFactor: CGFloat) { 448 | centering(transform: CGAffineTransform(scaleX: xFactor, y: yFactor)) 449 | } 450 | 451 | public func fit(to destRect: CGRect) { 452 | let bounds = boundingBox 453 | let fitRect = bounds.filling(in: destRect) 454 | let factor = min(destRect.size.width / bounds.size.width, destRect.size.height / bounds.size.height) 455 | moveCenter(to: fitRect.center) 456 | scale(xFactor: factor, yFactor: factor) 457 | } 458 | 459 | public func horizontalInverst() { 460 | centering(transform: CGAffineTransform(scaleX: -1, y: 1)) 461 | } 462 | 463 | public func verticalInverst() { 464 | centering(transform: CGAffineTransform(scaleX: 1, y: -1)) 465 | } 466 | 467 | public class func make(string: NSString, font: UIFont) -> UIBezierPath? { 468 | // Initialize path 469 | let path = UIBezierPath() 470 | if (0 == string.length) { 471 | return path 472 | } 473 | // Create font ref 474 | let fontRef = CTFontCreateWithName((font.fontName as CFString?)!, font.pointSize, nil) 475 | // Create glyphs (that is, individual letter shapes) 476 | 477 | 478 | var characters = [UniChar]() 479 | let length = (string as NSString).length 480 | for i in stride(from: 0, to: length, by: 1) { 481 | characters.append((string as NSString).character(at: i)) 482 | } 483 | 484 | let glyphs = UnsafeMutablePointer.allocate(capacity: length) 485 | glyphs.initialize(to: 0) 486 | 487 | let success = CTFontGetGlyphsForCharacters(font, characters, glyphs, length) 488 | if (!success) { 489 | logging("Error retrieving string glyphs") 490 | free(glyphs) 491 | return nil 492 | } 493 | 494 | // Draw each char into path 495 | for i in 0 ..< string.length { 496 | // Glyph to CGPath 497 | let glyph = glyphs[i] 498 | guard let pathRef = CTFontCreatePathForGlyph(fontRef, glyph, nil) else { 499 | return nil 500 | } 501 | 502 | // Append CGPath 503 | path.append(UIBezierPath(cgPath: pathRef)) 504 | 505 | // Offset by size 506 | let size = (string.substring(with: NSRange( i ..< i + 1)) as NSString).size(withAttributes: [NSAttributedStringKey.font: font]) 507 | path.offset(vector: CGVector(dx: -size.width, dy: 0)) 508 | } 509 | 510 | // Clean up 511 | free(glyphs) 512 | 513 | // Return the path to the UIKit coordinate system MirrorPathVertically(path); 514 | return path 515 | } 516 | 517 | public class func makePolygon(numberOfSides: Int) -> UIBezierPath? { 518 | if numberOfSides < 3 { 519 | logging("Error: Please supply at least 3 sides") 520 | return nil 521 | } 522 | 523 | let path = UIBezierPath() 524 | 525 | // Use a unit rectangle as the destination 526 | let destinationRect = CGRect(x: 0, y: 0, width: 1, height: 1) 527 | let center = CGRectGetCenter(destinationRect) 528 | let radius: CGFloat = 0.5 529 | 530 | var firstPoint = true 531 | for i in 0 ..< numberOfSides - 1 { 532 | let theta: Double = Double.pi + Double(i) * 2 * Double.pi / Double(numberOfSides) 533 | let dTheta: Double = 2 * Double.pi / Double(numberOfSides) 534 | var point = CGPoint.zero 535 | if firstPoint { 536 | point.x = center.x + radius * CGFloat(sin(theta)) 537 | point.y = center.y + radius * CGFloat(cos(dTheta)) 538 | firstPoint = false 539 | } 540 | point.x = center.x + radius * CGFloat(sin(theta + dTheta)) 541 | point.y = center.y + radius * CGFloat(cos(theta + dTheta)) 542 | 543 | path.addLine(to: point) 544 | } 545 | path.close() 546 | 547 | return path 548 | } 549 | } 550 | 551 | /// Query context for size and use screen scale to map from Quartz pixels to UIKit points 552 | public func UIKitGetContextSize() -> CGSize { 553 | guard let context = UIGraphicsGetCurrentContext() else { 554 | return CGSize.zero 555 | } 556 | 557 | let size = CGSize( 558 | width: CGFloat(context.width), 559 | height: CGFloat(context.height) 560 | ) 561 | let scale: CGFloat = UIScreen.main.scale 562 | return CGSize( 563 | width: size.width / scale, 564 | height: size.height / scale 565 | ) 566 | } 567 | 568 | public extension CGContext { 569 | /// Horizontal flip context by supplying the size 570 | public func horizontalInverst(size: CGSize) { 571 | textMatrix = CGAffineTransform.identity 572 | translateBy(x: size.width, y: 0) 573 | scaleBy(x: -1.0, y: 1.0) 574 | } 575 | 576 | /// Vertical flip context by supplying the size 577 | public func verticalInverst(size: CGSize) { 578 | textMatrix = CGAffineTransform.identity 579 | translateBy(x: 0, y: size.height) 580 | scaleBy(x: 1.0, y: -1.0) 581 | } 582 | 583 | /// Flip context by retrieving image 584 | @discardableResult 585 | public func horizontalInverstImage() -> Bool { 586 | if let image = UIGraphicsGetImageFromCurrentImageContext() { 587 | horizontalInverst(size: image.size) 588 | return true 589 | } 590 | return false 591 | } 592 | 593 | /// Flip context by retrieving image 594 | @discardableResult 595 | public func verticalInverstImage() -> Bool { 596 | if let image = UIGraphicsGetImageFromCurrentImageContext() { 597 | verticalInverst(size: image.size) 598 | return true 599 | } 600 | return false 601 | } 602 | } 603 | --------------------------------------------------------------------------------