├── .github ├── FUNDING.yml ├── PULL_REQUEST_TEMPLATE.md └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── Sources └── SwiftBoost │ ├── Foundation │ ├── Extensions │ │ ├── NSObjectExtension.swift │ │ ├── URLExtension.swift │ │ ├── CollectionExtension.swift │ │ ├── NotificationCenterExtension.swift │ │ ├── BoolExtension.swift │ │ ├── UserDefaultsExtension.swift │ │ ├── CalendarExtension.swift │ │ ├── MutableCollectionExtension.swift │ │ ├── FileManager │ │ │ ├── FileManagerDestination.swift │ │ │ └── FileManagerExtension.swift │ │ ├── ArrayExtension.swift │ │ ├── LocaleExtension.swift │ │ ├── MirrorExtension.swift │ │ ├── StringExtension.swift │ │ ├── URLSessionExtension.swift │ │ └── DateExtension.swift │ ├── Typealiases │ │ └── ClosuresExtension.swift │ ├── Delay.swift │ ├── Do.swift │ └── Logger.swift │ ├── UIKit │ └── Extensions │ │ ├── UIGestureRecognizerExtension.swift │ │ ├── UITextFieldExtension.swift │ │ ├── UIVisualEffectViewExtension.swift │ │ ├── UIScreenExtension.swift │ │ ├── UIEdgeInsetsExtension.swift │ │ ├── NSDirectionalEdgeInsetsExtension.swift │ │ ├── UIDeviceExtension.swift │ │ ├── UIButtonExtension.swift │ │ ├── UIBezierPathExtension.swift │ │ ├── UISegmentedControlExtension.swift │ │ ├── UISliderExtension.swift │ │ ├── UITextViewExtension.swift │ │ ├── UITabBarControllerExtension.swift │ │ ├── UINavigationControllerExtension.swift │ │ ├── UIImageViewExtension.swift │ │ ├── UIScrollViewExtension.swift │ │ ├── UIFeedbackGeneratorExtension.swift │ │ ├── UILabelExtension.swift │ │ ├── UIFontExtension.swift │ │ ├── UITableViewExtension.swift │ │ ├── UIApplicationExtension.swift │ │ ├── UICollectionViewExtension.swift │ │ ├── UITabBarExtension.swift │ │ ├── UIToolbarExtension.swift │ │ ├── UINavigationBarExtension.swift │ │ ├── UIImageExtension.swift │ │ ├── UIAlertControllerExtension.swift │ │ ├── UIViewControllerExtension.swift │ │ ├── UIColorExtension.swift │ │ └── UIViewExtension.swift │ └── CoreGraphics │ └── Extensions │ ├── CGFloatExtension.swift │ ├── CGAffineTransformExtension.swift │ ├── CGSizeExtension.swift │ ├── CGPointExtension.swift │ └── CGRectExtension.swift ├── Package.swift ├── CONTRIBUTING.md ├── SwiftBoost.podspec ├── LICENSE ├── README.md └── CODE_OF_CONDUCT.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [sparrowcode] 2 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Goal 2 | 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # macOS Files 2 | .DS_Store 3 | .Trashes 4 | 5 | # Swift Package Manager 6 | .swiftpm 7 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: ivanvorobei 7 | --- 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: ivanvorobei 7 | --- 8 | -------------------------------------------------------------------------------- /Sources/SwiftBoost/Foundation/Extensions/NSObjectExtension.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public extension NSObject { 4 | 5 | var className: String { String(describing: type(of: self)) } 6 | } 7 | -------------------------------------------------------------------------------- /Sources/SwiftBoost/Foundation/Extensions/URLExtension.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public extension URL { 4 | 5 | static var empty: URL { 6 | return URL(string: "https://apple.com")! 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Sources/SwiftBoost/Foundation/Typealiases/ClosuresExtension.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public typealias ClosureVoid = ()->Void 4 | public typealias ClosureBool = (Bool)->Void 5 | public typealias ClosureString = (String)->Void 6 | public typealias ClosureInt = (Int)->Void 7 | -------------------------------------------------------------------------------- /Sources/SwiftBoost/Foundation/Delay.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public func delay(_ delay: TimeInterval, closure: @escaping ClosureVoid) { 4 | let when = DispatchTime.now() + delay 5 | DispatchQueue.main.asyncAfter(deadline: when) { 6 | closure() 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Sources/SwiftBoost/UIKit/Extensions/UIGestureRecognizerExtension.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) && (os(iOS) || os(tvOS)) 2 | import UIKit 3 | 4 | public extension UIGestureRecognizer { 5 | 6 | func removeFromView() { 7 | view?.removeGestureRecognizer(self) 8 | } 9 | } 10 | #endif 11 | -------------------------------------------------------------------------------- /Sources/SwiftBoost/UIKit/Extensions/UITextFieldExtension.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) && (os(iOS) || os(tvOS)) 2 | import UIKit 3 | 4 | public extension UITextField { 5 | 6 | func removeTargetsAndActions() { 7 | self.removeTarget(nil, action: nil, for: .allEvents) 8 | } 9 | } 10 | #endif 11 | -------------------------------------------------------------------------------- /Sources/SwiftBoost/CoreGraphics/Extensions/CGFloatExtension.swift: -------------------------------------------------------------------------------- 1 | #if canImport(CoreGraphics) 2 | import CoreGraphics 3 | 4 | extension CGFloat { 5 | 6 | public func rounded(to places: Int) -> CGFloat { 7 | let divisor = pow(10.0, CGFloat(places)) 8 | return (self * divisor).rounded() / divisor 9 | } 10 | } 11 | #endif 12 | -------------------------------------------------------------------------------- /Sources/SwiftBoost/UIKit/Extensions/UIVisualEffectViewExtension.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) && (os(iOS) || os(tvOS)) 2 | import UIKit 3 | 4 | public extension UIVisualEffectView { 5 | 6 | convenience init(style: UIBlurEffect.Style) { 7 | let effect = UIBlurEffect(style: style) 8 | self.init(effect: effect) 9 | } 10 | } 11 | #endif 12 | -------------------------------------------------------------------------------- /Sources/SwiftBoost/UIKit/Extensions/UIScreenExtension.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) && (os(iOS)) 2 | import UIKit 3 | 4 | public extension UIScreen { 5 | 6 | var displayCornerRadius: CGFloat { 7 | guard let cornerRadius = self.value(forKey: "_displayCornerRadius") as? CGFloat else { 8 | return .zero 9 | } 10 | return cornerRadius 11 | } 12 | } 13 | #endif 14 | -------------------------------------------------------------------------------- /Sources/SwiftBoost/UIKit/Extensions/UIEdgeInsetsExtension.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) 2 | import UIKit 3 | 4 | public extension UIEdgeInsets { 5 | 6 | init(horizontal: CGFloat, vertical: CGFloat) { 7 | self.init(top: vertical, left: horizontal, bottom: vertical, right: horizontal) 8 | } 9 | 10 | init(side: CGFloat) { 11 | self.init(top: side, left: side, bottom: side, right: side) 12 | } 13 | } 14 | #endif 15 | -------------------------------------------------------------------------------- /Sources/SwiftBoost/Foundation/Extensions/CollectionExtension.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public extension Collection { 4 | 5 | /** 6 | SwiftBoost: Getting object by `index` safety. 7 | 8 | if object not exist, returned nil. Before use need safety unwrap. 9 | 10 | - parameter index: Index of object. 11 | */ 12 | subscript (safe index: Index) -> Element? { 13 | return indices.contains(index) ? self[index] : nil 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/SwiftBoost/UIKit/Extensions/NSDirectionalEdgeInsetsExtension.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) 2 | import UIKit 3 | 4 | public extension NSDirectionalEdgeInsets { 5 | 6 | init(_ insets: UIEdgeInsets) { 7 | self.init(top: insets.top, leading: insets.left, bottom: insets.bottom, trailing: insets.right) 8 | } 9 | 10 | var edgeInstset: UIEdgeInsets { 11 | return .init(top: top, left: leading, bottom: bottom, right: trailing) 12 | } 13 | } 14 | #endif 15 | -------------------------------------------------------------------------------- /Sources/SwiftBoost/UIKit/Extensions/UIDeviceExtension.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) && (os(iOS) || os(tvOS)) 2 | import UIKit 3 | 4 | public extension UIDevice { 5 | 6 | var isMac: Bool { 7 | if #available(iOS 14.0, tvOS 14.0, *) { 8 | if UIDevice.current.userInterfaceIdiom == .mac { 9 | return true 10 | } 11 | } 12 | #if targetEnvironment(macCatalyst) 13 | return true 14 | #else 15 | return false 16 | #endif 17 | } 18 | } 19 | #endif 20 | -------------------------------------------------------------------------------- /Sources/SwiftBoost/Foundation/Extensions/NotificationCenterExtension.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension NotificationCenter { 4 | 5 | public func addObserver(_ names: NSNotification.Name..., using block: @escaping @Sendable (Notification) -> Void) { 6 | for name in names { 7 | NotificationCenter.default.addObserver(forName: name, object: nil, queue: nil, using: block) 8 | } 9 | } 10 | 11 | public func post(name: NSNotification.Name) { 12 | post(name: name, object: nil) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Sources/SwiftBoost/Foundation/Extensions/BoolExtension.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public extension Bool { 4 | 5 | /** 6 | SwiftBoost: Convert bool to Int. 7 | 8 | If `Bool` is true, `Int` is equal 1. Else is equal 0. 9 | */ 10 | var int: Int { 11 | return self ? 1 : 0 12 | } 13 | 14 | /** 15 | SwiftBoost: Convert bool to String. 16 | 17 | If `Bool` is true, `String` is equal "true". Else is equal "false". 18 | */ 19 | var string: String { 20 | return self ? "true" : "false" 21 | } 22 | } 23 | 24 | -------------------------------------------------------------------------------- /Sources/SwiftBoost/UIKit/Extensions/UIButtonExtension.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) && (os(iOS) || os(tvOS)) 2 | import UIKit 3 | 4 | public extension UIButton { 5 | 6 | func setTitle(_ title: String) { 7 | setTitle(title, for: .normal) 8 | } 9 | 10 | func setImage(_ image: UIImage?) { 11 | setImage(image, for: .normal) 12 | setImage(image, for: .highlighted) 13 | setImage(image, for: .disabled) 14 | } 15 | 16 | func removeTargetsAndActions() { 17 | self.removeTarget(nil, action: nil, for: .allEvents) 18 | } 19 | } 20 | #endif 21 | -------------------------------------------------------------------------------- /Sources/SwiftBoost/UIKit/Extensions/UIBezierPathExtension.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) && (os(iOS) || os(tvOS)) 2 | import UIKit 3 | 4 | public extension UIBezierPath { 5 | 6 | convenience init(from: CGPoint, to otherPoint: CGPoint) { 7 | self.init() 8 | move(to: from) 9 | addLine(to: otherPoint) 10 | } 11 | 12 | convenience init(points: [CGPoint]) { 13 | self.init() 14 | if !points.isEmpty { 15 | move(to: points[0]) 16 | for point in points[1...] { 17 | addLine(to: point) 18 | } 19 | } 20 | } 21 | } 22 | #endif 23 | -------------------------------------------------------------------------------- /Sources/SwiftBoost/UIKit/Extensions/UISegmentedControlExtension.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) && (os(iOS) || os(tvOS)) 2 | import UIKit 3 | 4 | public extension UISegmentedControl { 5 | 6 | var segmentTitles: [String] { 7 | get { 8 | let range = 0.. Date? { 6 | return object(forKey: key) as? Date 7 | } 8 | 9 | func set(object: T, forKey: String) throws { 10 | let jsonData = try? JSONEncoder().encode(object) 11 | set(jsonData, forKey: forKey) 12 | } 13 | 14 | func get(objectType: T.Type, forKey: String) throws -> T? { 15 | guard let result = value(forKey: forKey) as? Data else { return nil } 16 | return try JSONDecoder().decode(objectType, from: result) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /SwiftBoost.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | 3 | s.name = 'SwiftBoost' 4 | s.version = '4.0.8' 5 | s.summary = 'Collection of Swift-extensions to boost development process.' 6 | s.homepage = 'https://github.com/sparrowcode/SwiftBoost' 7 | s.source = { :git => 'https://github.com/sparrowcode/SwiftBoost.git', :tag => s.version } 8 | s.license = { :type => "MIT", :file => "LICENSE" } 9 | s.author = { "Sparrow Code" => "hello@sparrowcode.io" } 10 | 11 | s.swift_version = '5.1' 12 | s.ios.deployment_target = '13.0' 13 | s.tvos.deployment_target = '13.0' 14 | s.watchos.deployment_target = '6.0' 15 | 16 | s.source_files = 'Sources/SwiftBoost/**/*.swift' 17 | 18 | end 19 | -------------------------------------------------------------------------------- /Sources/SwiftBoost/CoreGraphics/Extensions/CGAffineTransformExtension.swift: -------------------------------------------------------------------------------- 1 | #if canImport(CoreGraphics) 2 | import CoreGraphics 3 | 4 | public extension CGAffineTransform { 5 | 6 | init(scale: CGFloat) { 7 | self.init(scaleX: scale, y: scale) 8 | } 9 | 10 | init(rotationDegress: CGFloat) { 11 | self.init(rotationAngle: CGFloat(rotationDegress * .pi / 180)) 12 | } 13 | 14 | func scaledBy(xy: CGFloat) -> CGAffineTransform { 15 | return self.scaledBy(x: xy, y: xy) 16 | } 17 | 18 | func rotated(degress: CGFloat) -> CGAffineTransform { 19 | return self.rotated(by: CGFloat(degress * .pi / 180)) 20 | } 21 | } 22 | #endif 23 | -------------------------------------------------------------------------------- /Sources/SwiftBoost/UIKit/Extensions/UISliderExtension.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) && os(iOS) 2 | import UIKit 3 | 4 | public extension UISlider { 5 | 6 | func setValue( 7 | _ value: Float, 8 | animated: Bool = true, 9 | duration: TimeInterval, 10 | completion: (() -> Void)? = nil) { 11 | 12 | if animated { 13 | UIView.animate(withDuration: duration, animations: { 14 | self.setValue(value, animated: true) 15 | }, completion: { _ in 16 | completion?() 17 | }) 18 | } else { 19 | setValue(value, animated: false) 20 | completion?() 21 | } 22 | } 23 | } 24 | #endif 25 | -------------------------------------------------------------------------------- /Sources/SwiftBoost/UIKit/Extensions/UITextViewExtension.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) && (os(iOS) || os(tvOS)) 2 | import UIKit 3 | 4 | public extension UITextView { 5 | 6 | func layoutDynamicHeight(width: CGFloat) { 7 | // Requerid for dynamic height. 8 | if isScrollEnabled { isScrollEnabled = false } 9 | 10 | frame.setWidth(width) 11 | sizeToFit() 12 | if frame.width != width { 13 | frame.setWidth(width) 14 | } 15 | } 16 | 17 | func layoutDynamicHeight(x: CGFloat, y: CGFloat, width: CGFloat) { 18 | // Requerid for dynamic height. 19 | if isScrollEnabled { isScrollEnabled = false } 20 | 21 | frame = CGRect.init(x: x, y: y, width: width, height: frame.height) 22 | sizeToFit() 23 | if frame.width != width { 24 | frame.setWidth(width) 25 | } 26 | } 27 | } 28 | #endif 29 | -------------------------------------------------------------------------------- /Sources/SwiftBoost/CoreGraphics/Extensions/CGSizeExtension.swift: -------------------------------------------------------------------------------- 1 | #if canImport(CoreGraphics) 2 | import CoreGraphics 3 | 4 | public extension CGSize { 5 | 6 | init(side: CGFloat) { 7 | self.init(width: side, height: side) 8 | } 9 | 10 | var aspectRatio: CGFloat { 11 | guard width != .zero, height != .zero else { return .zero } 12 | return width / height 13 | } 14 | 15 | var maxDimension: CGFloat { max(width, height) } 16 | var minDimension: CGFloat { min(width, height) } 17 | 18 | func resize(newWidth: CGFloat) -> CGSize { 19 | let scaleFactor = newWidth / self.width 20 | let newHeight = self.height * scaleFactor 21 | return CGSize(width: newWidth, height: newHeight) 22 | } 23 | 24 | func resize(newHeight: CGFloat) -> CGSize { 25 | let scaleFactor = newHeight / self.height 26 | let newWidth = self.width * scaleFactor 27 | return CGSize(width: newWidth, height: newHeight) 28 | } 29 | } 30 | #endif 31 | -------------------------------------------------------------------------------- /Sources/SwiftBoost/UIKit/Extensions/UITabBarControllerExtension.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) && (os(iOS) || os(tvOS)) 2 | import UIKit 3 | 4 | public extension UITabBarController { 5 | 6 | func addTabBarItem(with controller: UIViewController, title: String, image: UIImage, selectedImage: UIImage? = nil) { 7 | let tabBarItem = UITabBarItem(title: title, image: image, selectedImage: selectedImage ?? image) 8 | controller.tabBarItem = tabBarItem 9 | if self.viewControllers == nil { self.viewControllers = [controller] } 10 | else { self.viewControllers?.append(controller) } 11 | } 12 | 13 | func addTabBarItem(with controller: UIViewController, _ item: UITabBarItem.SystemItem, tag: Int) { 14 | let tabBarItem = UITabBarItem.init(tabBarSystemItem: item, tag: tag) 15 | controller.tabBarItem = tabBarItem 16 | if self.viewControllers == nil { self.viewControllers = [controller] } 17 | else { self.viewControllers?.append(controller) } 18 | } 19 | } 20 | #endif 21 | -------------------------------------------------------------------------------- /Sources/SwiftBoost/UIKit/Extensions/UINavigationControllerExtension.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) && (os(iOS) || os(tvOS)) 2 | import UIKit 3 | 4 | public extension UINavigationController { 5 | 6 | convenience init(rootViewController: UIViewController, prefersLargeTitles: Bool) { 7 | self.init(rootViewController: rootViewController) 8 | #if os(iOS) 9 | navigationBar.prefersLargeTitles = prefersLargeTitles 10 | #endif 11 | } 12 | 13 | 14 | func popViewController(animated: Bool = true, _ completion: (() -> Void)? = nil) { 15 | CATransaction.begin() 16 | CATransaction.setCompletionBlock(completion) 17 | popViewController(animated: animated) 18 | CATransaction.commit() 19 | } 20 | 21 | func pushViewController(_ viewController: UIViewController, completion: (() -> Void)? = nil) { 22 | CATransaction.begin() 23 | CATransaction.setCompletionBlock(completion) 24 | pushViewController(viewController, animated: true) 25 | CATransaction.commit() 26 | } 27 | } 28 | #endif 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Sparrow Code 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/SwiftBoost/Foundation/Extensions/CalendarExtension.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public extension Calendar.Component { 4 | /** 5 | SwiftBoost: Format components. 6 | 7 | Take a look at this example: 8 | ``` 9 | Calendar.Component.month.formatted(numberOfUnits: 2, unitsStyle: .full) // 2 months 10 | Calendar.Component.day.formatted(numberOfUnits: 15, unitsStyle: .short) // 15 days 11 | Calendar.Component.year.formatted(numberOfUnits: 1, unitsStyle: .abbreviated) // 1y 12 | ``` 13 | 14 | - parameter numberOfUnits: Count of units of component. 15 | - parameter unitsStyle: Style of formatting of units. 16 | */ 17 | func formatted(numberOfUnits: Int, unitsStyle: DateComponentsFormatter.UnitsStyle = .full) -> String? { 18 | let formatter = DateComponentsFormatter() 19 | formatter.maximumUnitCount = 1 20 | formatter.unitsStyle = unitsStyle 21 | formatter.zeroFormattingBehavior = .dropAll 22 | var dateComponents = DateComponents() 23 | dateComponents.setValue(numberOfUnits, for: self) 24 | return formatter.string(from: dateComponents) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/SwiftBoost/Foundation/Extensions/MutableCollectionExtension.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public extension MutableCollection where Self: RandomAccessCollection { 4 | 5 | func sorted( 6 | by keyPath: KeyPath, 7 | using valuesAreInIncreasingOrder: (Value, Value) throws -> Bool 8 | ) rethrows -> [Element] { 9 | try sorted { 10 | try valuesAreInIncreasingOrder($0[keyPath: keyPath], $1[keyPath: keyPath]) 11 | } 12 | } 13 | 14 | func sorted( 15 | by keyPath: KeyPath, 16 | order: MutableCollectionOrder 17 | ) -> [Element] { 18 | sorted(by: keyPath, using: order.operator) 19 | } 20 | } 21 | 22 | public enum MutableCollectionOrder { 23 | 24 | // MARK: - Cases 25 | 26 | /// Represents ascending order. In this case, the associated operator function is `<`. 27 | case ascending 28 | /// Represents descending order. In this case, the associated operator function is `>`. 29 | case descending 30 | 31 | // MARK: - Properties 32 | 33 | public var `operator`: (Value, Value) -> Bool { 34 | switch self { 35 | case .ascending: 36 | return (<) 37 | case .descending: 38 | return (>) 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Sources/SwiftBoost/Foundation/Do.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | #if !os(Linux) 4 | import CoreGraphics 5 | #endif 6 | 7 | #if os(iOS) || os(tvOS) 8 | import UIKit.UIGeometry 9 | #endif 10 | 11 | public protocol Do {} 12 | 13 | extension Do where Self: Any { 14 | 15 | /** 16 | SwiftBoost: Synaxic sugar for code reduction. Access instance or object properties using `do` via closures 17 | ``` 18 | let titleLabel = UILabel().do { 19 | $0.font = .systemFont(ofSize: 34, weight: .bold) 20 | $0.textColor = .systemRed 21 | } 22 | ``` 23 | --- or --- 24 | ``` 25 | let titleLabel = UILabel() 26 | titleLabel.do { 27 | $0.font = .systemFont(ofSize: 34, weight: .bold) 28 | $0.textColor = .systemRed 29 | } 30 | ``` 31 | */ 32 | @discardableResult @inlinable 33 | public func `do`(_ block: (Self) throws -> Void) rethrows -> Self { 34 | try block(self) 35 | return self 36 | } 37 | 38 | } 39 | 40 | extension NSObject: Do {} 41 | 42 | #if !os(Linux) 43 | extension CGPoint: Do {} 44 | extension CGRect: Do {} 45 | extension CGSize: Do {} 46 | extension CGVector: Do {} 47 | #endif 48 | 49 | extension Array: Do {} 50 | extension Dictionary: Do {} 51 | extension Set: Do {} 52 | 53 | #if os(iOS) || os(tvOS) 54 | extension UIEdgeInsets: Do {} 55 | extension UIOffset: Do {} 56 | extension UIRectEdge: Do {} 57 | #endif 58 | -------------------------------------------------------------------------------- /Sources/SwiftBoost/Foundation/Extensions/FileManager/FileManagerDestination.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct FileManagerDestination { 4 | 5 | // MARK: - Data 6 | 7 | public var file: String 8 | public var directory: URL 9 | 10 | public var url: URL { directory.appendingPathComponent(file) } 11 | 12 | // MARK: - Init 13 | 14 | public init(directory: FileManager.SearchPathDirectory, path: String, file: String) { 15 | let fileManager = FileManager.default 16 | do { 17 | let documentDirectory = try fileManager.url(for: directory, in: .userDomainMask, appropriateFor: nil, create: true) 18 | self.directory = documentDirectory.appendingPathComponent(Self.cleaned(path)) 19 | } catch { 20 | print(error.localizedDescription) 21 | fatalError() 22 | } 23 | self.file = file 24 | } 25 | 26 | public init(fileName: String) { 27 | let bundleFileURL = Bundle.main.url(forResource: fileName, withExtension: nil) 28 | self.directory = bundleFileURL?.deletingLastPathComponent() ?? URL.init(string: .empty)! 29 | self.file = bundleFileURL?.lastPathComponent ?? .empty 30 | } 31 | 32 | // MARK: - Private 33 | 34 | private static func cleaned(_ path: String) -> String { 35 | return path.removedPrefix("/").removedSuffix("/") 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/SwiftBoost/CoreGraphics/Extensions/CGPointExtension.swift: -------------------------------------------------------------------------------- 1 | #if canImport(CoreGraphics) 2 | import CoreGraphics 3 | 4 | public extension CGPoint { 5 | 6 | func distance(from point: CGPoint) -> CGFloat { 7 | return CGPoint.distance(from: self, to: point) 8 | } 9 | 10 | static func distance(from point1: CGPoint, to point2: CGPoint) -> CGFloat { 11 | return sqrt(pow(point2.x - point1.x, 2) + pow(point2.y - point1.y, 2)) 12 | } 13 | 14 | // MARK: - Operators 15 | 16 | static func + (lhs: CGPoint, rhs: CGPoint) -> CGPoint { 17 | return CGPoint(x: lhs.x + rhs.x, y: lhs.y + rhs.y) 18 | } 19 | 20 | static func += (lhs: inout CGPoint, rhs: CGPoint) { 21 | lhs = lhs + rhs 22 | } 23 | 24 | static func - (lhs: CGPoint, rhs: CGPoint) -> CGPoint { 25 | return CGPoint(x: lhs.x - rhs.x, y: lhs.y - rhs.y) 26 | } 27 | 28 | static func -= (lhs: inout CGPoint, rhs: CGPoint) { 29 | lhs = lhs - rhs 30 | } 31 | 32 | static func * (point: CGPoint, scalar: CGFloat) -> CGPoint { 33 | return CGPoint(x: point.x * scalar, y: point.y * scalar) 34 | } 35 | 36 | static func *= (point: inout CGPoint, scalar: CGFloat) { 37 | point = point * scalar 38 | } 39 | 40 | static func * (scalar: CGFloat, point: CGPoint) -> CGPoint { 41 | return CGPoint(x: point.x * scalar, y: point.y * scalar) 42 | } 43 | } 44 | #endif 45 | -------------------------------------------------------------------------------- /Sources/SwiftBoost/UIKit/Extensions/UIImageViewExtension.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) && (os(iOS) || os(tvOS)) 2 | import UIKit 3 | 4 | public extension UIImageView { 5 | 6 | func download( 7 | from url: URL, 8 | contentMode: UIView.ContentMode = .scaleAspectFit, 9 | placeholder: UIImage? = nil, 10 | completionHandler: ((UIImage?) -> Void)? = nil) { 11 | 12 | image = placeholder 13 | self.contentMode = contentMode 14 | URLSession.shared.dataTask(with: url) { data, response, _ in 15 | guard 16 | let httpURLResponse = response as? HTTPURLResponse, httpURLResponse.statusCode == 200, 17 | let mimeType = response?.mimeType, mimeType.hasPrefix("image"), 18 | let data = data, 19 | let image = UIImage(data: data) else { 20 | completionHandler?(nil) 21 | return 22 | } 23 | DispatchQueue.main.async { [unowned self] in 24 | self.image = image 25 | completionHandler?(image) 26 | } 27 | }.resume() 28 | } 29 | 30 | func blur(withStyle style: UIBlurEffect.Style) { 31 | let blurEffect = UIBlurEffect(style: style) 32 | let blurEffectView = UIVisualEffectView(effect: blurEffect) 33 | blurEffectView.frame = bounds 34 | blurEffectView.autoresizingMask = [.flexibleWidth, .flexibleHeight] 35 | addSubview(blurEffectView) 36 | clipsToBounds = true 37 | } 38 | } 39 | #endif 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SwiftBoost 2 | 3 | Collection of Swift-extensions to boost development process. 4 | 5 | ### iOS Dev Community 6 | 7 |

8 | 9 | 10 | 11 |

12 | 13 | ## Installation 14 | 15 | Ready to use on iOS 13+, tvOS 13+, watchOS 6.0+. 16 | 17 | ### Swift Package Manager 18 | 19 | In Xcode go to Project -> Your Project Name -> `Package Dependencies` -> Tap *Plus*. Insert url: 20 | 21 | ``` 22 | https://github.com/sparrowcode/SwiftBoost 23 | ``` 24 | 25 | or adding it to the `dependencies` of your `Package.swift`: 26 | 27 | ```swift 28 | dependencies: [ 29 | .package(url: "https://github.com/sparrowcode/SwiftBoost", .upToNextMajor(from: "4.0.8")) 30 | ] 31 | ``` 32 | 33 | ### CocoaPods: 34 | 35 | This is an outdated way of doing things. I advise you to use [SPM](#swift-package-manager). However, I will continue to support Cocoapods for some time. 36 | 37 |
Cocoapods Instalation 38 | 39 | [CocoaPods](https://cocoapods.org) is a dependency manager. For usage and installation instructions, visit their website. To integrate using CocoaPods, specify it in your `Podfile`: 40 | 41 | ```ruby 42 | pod 'SwiftBoost' 43 | ``` 44 |
45 | 46 | ### Manually 47 | 48 | If you prefer not to use any of dependency managers, you can integrate manually. Put `Sources/SwiftBoost` folder in your Xcode project. Make sure to enable `Copy items if needed` and `Create groups`. 49 | -------------------------------------------------------------------------------- /Sources/SwiftBoost/Foundation/Extensions/ArrayExtension.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public extension Array { 4 | 5 | /** 6 | SwiftBoost: 7 | */ 8 | func rearrange(fromIndex: Int, toIndex: Int) -> [Element] { 9 | var array = self 10 | let element = array.remove(at: fromIndex) 11 | array.insert(element, at: toIndex) 12 | return array 13 | } 14 | 15 | /** 16 | SwiftBoost: Split array of elements into chunks of a size specify. Example: 17 | ``` 18 | let array = [1,2,3,4,5,6,7] 19 | array.chuncked(by: 3) // [[1,2,3], [4,5,6], [7]] 20 | ``` 21 | 22 | - parameter chunkSize: Subarray size. 23 | */ 24 | func chunked(by chunkSize: Int) -> [[Element]] { 25 | return stride(from: 0, to: self.count, by: chunkSize).map { 26 | Array(self[$0.. [Element] { 43 | var result = [Element]() 44 | for value in self { 45 | if result.contains(value) == false { 46 | result.append(value) 47 | } 48 | } 49 | return result 50 | } 51 | } 52 | 53 | public extension ArraySlice { 54 | 55 | var array: [Element] { 56 | return Array(self) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Sources/SwiftBoost/Foundation/Extensions/LocaleExtension.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public extension Locale { 4 | 5 | var is12HourTimeFormat: Bool { 6 | let dateFormatter = DateFormatter() 7 | dateFormatter.timeStyle = .short 8 | dateFormatter.dateStyle = .none 9 | dateFormatter.locale = self 10 | let dateString = dateFormatter.string(from: Date()) 11 | return dateString.contains(dateFormatter.amSymbol) || dateString.contains(dateFormatter.pmSymbol) 12 | } 13 | 14 | var languageID: String? { 15 | if #available(iOS 16.0, tvOS 16.0, watchOS 9.0, macOS 13.0, *) { 16 | return self.language.languageCode?.identifier 17 | } else { 18 | return self.languageCode 19 | } 20 | } 21 | 22 | func localised(in locale: Locale) -> String? { 23 | guard let currentLanguageCode = self.languageID else { return nil } 24 | guard let toLanguageCode = locale.languageID else { return nil } 25 | let nslocale = NSLocale(localeIdentifier: toLanguageCode) 26 | let text = nslocale.displayName(forKey: NSLocale.Key.identifier, value: currentLanguageCode) 27 | return text?.localizedCapitalized 28 | } 29 | 30 | static func flagEmoji(forRegionCode isoRegionCode: String) -> String? { 31 | guard isoRegionCodes.contains(isoRegionCode) else { return nil } 32 | return isoRegionCode.unicodeScalars.reduce(into: String()) { 33 | guard let flagScalar = UnicodeScalar(UInt32(127_397) + $1.value) else { return } 34 | $0.unicodeScalars.append(flagScalar) 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/SwiftBoost/UIKit/Extensions/UIScrollViewExtension.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) && (os(iOS) || os(tvOS)) 2 | import UIKit 3 | 4 | public extension UIScrollView { 5 | 6 | var visibleRect: CGRect { 7 | let contentWidth = contentSize.width - contentOffset.x 8 | let contentHeight = contentSize.height - contentOffset.y 9 | return CGRect( 10 | origin: contentOffset, 11 | size: CGSize( 12 | width: min(min(bounds.size.width, contentSize.width), contentWidth), 13 | height: min(min(bounds.size.height, contentSize.height), contentHeight) 14 | ) 15 | ) 16 | } 17 | 18 | enum Side { 19 | case top, bottom, left, right 20 | } 21 | 22 | func scrollTo(_ side: Side, animated: Bool) { 23 | let point: CGPoint 24 | switch side { 25 | case .top: 26 | if contentSize.height < bounds.height { return } 27 | point = CGPoint( 28 | x: contentOffset.x, 29 | y: -(contentInset.top + safeAreaInsets.top) 30 | ) 31 | case .bottom: 32 | if contentSize.height < bounds.height { return } 33 | point = CGPoint( 34 | x: contentOffset.x, 35 | y: max(0, contentSize.height - bounds.height) + contentInset.bottom + safeAreaInsets.bottom 36 | ) 37 | case .left: 38 | point = CGPoint(x: -contentInset.left, y: contentOffset.y) 39 | case .right: 40 | point = CGPoint(x: max(0, contentSize.width - bounds.width) + contentInset.right, y: contentOffset.y) 41 | } 42 | setContentOffset(point, animated: animated) 43 | } 44 | } 45 | #endif 46 | -------------------------------------------------------------------------------- /Sources/SwiftBoost/UIKit/Extensions/UIFeedbackGeneratorExtension.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) && os(iOS) 2 | import UIKit 3 | 4 | public extension UIFeedbackGenerator { 5 | 6 | static func impactOccurred(_ style: Style) { 7 | switch style { 8 | case .light: 9 | let generator = UIImpactFeedbackGenerator(style: .light) 10 | generator.impactOccurred() 11 | case .medium: 12 | let generator = UIImpactFeedbackGenerator(style: .medium) 13 | generator.impactOccurred() 14 | case .heavy: 15 | let generator = UIImpactFeedbackGenerator(style: .heavy) 16 | generator.impactOccurred() 17 | case .notificationError: 18 | let generator = UINotificationFeedbackGenerator() 19 | generator.notificationOccurred(UINotificationFeedbackGenerator.FeedbackType.error) 20 | case .notificationSuccess: 21 | let generator = UINotificationFeedbackGenerator() 22 | generator.notificationOccurred(UINotificationFeedbackGenerator.FeedbackType.success) 23 | case .notificationWarning: 24 | let generator = UINotificationFeedbackGenerator() 25 | generator.notificationOccurred(UINotificationFeedbackGenerator.FeedbackType.warning) 26 | case .selectionChanged: 27 | let generator = UISelectionFeedbackGenerator() 28 | generator.selectionChanged() 29 | } 30 | } 31 | 32 | enum Style { 33 | 34 | case light 35 | case medium 36 | case heavy 37 | 38 | case notificationError 39 | case notificationSuccess 40 | case notificationWarning 41 | 42 | case selectionChanged 43 | } 44 | } 45 | #endif 46 | -------------------------------------------------------------------------------- /Sources/SwiftBoost/UIKit/Extensions/UILabelExtension.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) && (os(iOS) || os(tvOS)) 2 | import UIKit 3 | 4 | public extension UILabel { 5 | 6 | // MARK: - Helpers 7 | 8 | var letterSpace: CGFloat { 9 | set { 10 | let attributedString: NSMutableAttributedString! 11 | if let currentAttrString = attributedText { 12 | attributedString = NSMutableAttributedString(attributedString: currentAttrString) 13 | } else { 14 | attributedString = NSMutableAttributedString(string: text ?? "") 15 | text = nil 16 | } 17 | attributedString.addAttribute(NSAttributedString.Key.kern, value: newValue, range: NSRange(location: 0, length: attributedString.length)) 18 | attributedText = attributedString 19 | } 20 | get { 21 | if let currentLetterSpace = attributedText?.attribute(NSAttributedString.Key.kern, at: 0, effectiveRange: .none) as? CGFloat { 22 | return currentLetterSpace 23 | } else { 24 | return 0 25 | } 26 | } 27 | } 28 | 29 | // MARK: - Layout 30 | 31 | func layoutDynamicHeight() { 32 | sizeToFit() 33 | } 34 | 35 | func layoutDynamicHeight(width: CGFloat) { 36 | frame.setWidth(width) 37 | sizeToFit() 38 | if frame.width != width { 39 | frame.setWidth(width) 40 | } 41 | } 42 | 43 | func layoutDynamicHeight(x: CGFloat, y: CGFloat, width: CGFloat) { 44 | frame = CGRect.init(x: x, y: y, width: width, height: frame.height) 45 | sizeToFit() 46 | if frame.width != width { 47 | frame.setWidth(width) 48 | } 49 | } 50 | } 51 | #endif 52 | -------------------------------------------------------------------------------- /Sources/SwiftBoost/UIKit/Extensions/UIFontExtension.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) && (os(iOS) || os(tvOS)) 2 | import UIKit 3 | 4 | public extension UIFont { 5 | 6 | var rounded: UIFont { 7 | if #available(iOS 13, tvOS 13, *) { 8 | guard let descriptor = fontDescriptor.withDesign(.rounded) else { return self } 9 | return UIFont(descriptor: descriptor, size: 0) 10 | } else { 11 | return self 12 | } 13 | } 14 | 15 | var monospaced: UIFont { 16 | if #available(iOS 13, tvOS 13, *) { 17 | guard let descriptor = fontDescriptor.withDesign(.monospaced) else { return self } 18 | return UIFont(descriptor: descriptor, size: 0) 19 | } else { 20 | return self 21 | } 22 | } 23 | 24 | var serif: UIFont { 25 | if #available(iOS 13, tvOS 13, *) { 26 | guard let descriptor = fontDescriptor.withDesign(.serif) else { return self } 27 | return UIFont(descriptor: descriptor, size: 0) 28 | } else { 29 | return self 30 | } 31 | } 32 | 33 | static func preferredFont(forTextStyle style: TextStyle, addPoints: CGFloat = .zero) -> UIFont { 34 | let referensFont = UIFont.preferredFont(forTextStyle: style) 35 | return referensFont.withSize(referensFont.pointSize + addPoints) 36 | } 37 | 38 | static func preferredFont(forTextStyle style: TextStyle, weight: Weight, addPoints: CGFloat = .zero) -> UIFont { 39 | let descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: style) 40 | let font = UIFont.systemFont(ofSize: descriptor.pointSize + addPoints, weight: weight) 41 | let metrics = UIFontMetrics(forTextStyle: style) 42 | return metrics.scaledFont(for: font) 43 | } 44 | } 45 | #endif 46 | -------------------------------------------------------------------------------- /Sources/SwiftBoost/UIKit/Extensions/UITableViewExtension.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) && (os(iOS) || os(tvOS)) 2 | import UIKit 3 | 4 | public extension UITableView { 5 | 6 | // MARK: - Helpers 7 | 8 | func isValidIndexPath(_ indexPath: IndexPath) -> Bool { 9 | return indexPath.section >= 0 && 10 | indexPath.row >= 0 && 11 | indexPath.section < numberOfSections && 12 | indexPath.row < numberOfRows(inSection: indexPath.section) 13 | } 14 | 15 | func safeScrollToRow(at indexPath: IndexPath, at scrollPosition: UITableView.ScrollPosition, animated: Bool) { 16 | guard isValidIndexPath(indexPath) else { return } 17 | scrollToRow(at: indexPath, at: scrollPosition, animated: animated) 18 | } 19 | 20 | // MARK: - Cell Registration 21 | 22 | func register(_ class: T.Type) { 23 | register(T.self, forCellReuseIdentifier: String(describing: `class`)) 24 | } 25 | 26 | func dequeueReusableCell(withClass class: T.Type, for indexPath: IndexPath) -> T { 27 | guard let cell = dequeueReusableCell(withIdentifier: String(describing: `class`), for: indexPath) as? T else { 28 | fatalError() 29 | } 30 | return cell 31 | } 32 | 33 | // MARK: - Header & Footer Registration 34 | 35 | func register(_ class: T.Type) { 36 | register(T.self, forHeaderFooterViewReuseIdentifier: String(describing: `class`)) 37 | } 38 | 39 | func dequeueReusableHeaderFooterView(withClass class: T.Type) -> T { 40 | guard let view = dequeueReusableHeaderFooterView(withIdentifier: String(describing: `class`)) as? T else { 41 | fatalError() 42 | } 43 | return view 44 | } 45 | } 46 | #endif 47 | -------------------------------------------------------------------------------- /Sources/SwiftBoost/CoreGraphics/Extensions/CGRectExtension.swift: -------------------------------------------------------------------------------- 1 | #if canImport(CoreGraphics) 2 | import CoreGraphics 3 | 4 | public extension CGRect { 5 | 6 | mutating func setMaxX(_ value: CGFloat) { 7 | origin.x = value - width 8 | } 9 | 10 | mutating func setMaxY(_ value: CGFloat) { 11 | origin.y = value - height 12 | } 13 | 14 | mutating func setWidth(_ width: CGFloat) { 15 | if width == self.width { return } 16 | self = CGRect.init(x: origin.x, y: origin.y, width: width, height: height) 17 | } 18 | 19 | mutating func setHeight(_ height: CGFloat) { 20 | if height == self.height { return } 21 | self = CGRect.init(x: origin.x, y: origin.y, width: width, height: height) 22 | } 23 | 24 | // MARK: - Init 25 | 26 | init(center: CGPoint, size: CGSize) { 27 | let origin = CGPoint(x: center.x - size.width / 2, y: center.y - size.height / 2) 28 | self.init(origin: origin, size: size) 29 | } 30 | 31 | init(x: CGFloat, maxY: CGFloat, width: CGFloat, height: CGFloat) { 32 | self.init(x: x, y: .zero, width: width, height: height) 33 | setMaxY(maxY) 34 | } 35 | 36 | init(maxX: CGFloat, y: CGFloat, width: CGFloat, height: CGFloat) { 37 | self.init(x: .zero, y: y, width: width, height: height) 38 | setMaxX(maxX) 39 | } 40 | 41 | init(maxX: CGFloat, maxY: CGFloat, width: CGFloat, height: CGFloat) { 42 | self.init(x: .zero, y: .zero, width: width, height: height) 43 | setMaxX(maxX) 44 | setMaxY(maxY) 45 | } 46 | 47 | init(x: CGFloat, y: CGFloat, side: CGFloat) { 48 | self.init(x: x, y: y, width: side, height: side) 49 | } 50 | 51 | init(side: CGFloat) { 52 | self.init(x: .zero, y: .zero, width: side, height: side) 53 | } 54 | } 55 | #endif 56 | -------------------------------------------------------------------------------- /Sources/SwiftBoost/Foundation/Extensions/MirrorExtension.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Mirror { 4 | /** 5 | This static method iterates over properties of a given object, applying a closure to each property that matches the specified type. 6 | 7 | - Parameters: 8 | - target: The object whose properties will be reflected. 9 | - type: The type of properties to which the closure should be applied. By default, it's `T.self`. 10 | - recursively: If set to `true`, the method will reflect properties of the target's properties recursively. Default value is `false`. 11 | - closure: The closure to apply to each property of the specified type. The closure takes a parameter of type `T`. 12 | 13 | - Note: This function uses **Swift's Mirror API** for reflection. Not all properties may be accessible for types that do not fully support reflection. 14 | 15 | Exmaple usage: 16 | ``` 17 | class MyClass { 18 | var myIntProperty: Int = 0 19 | var myStringProperty: String = "Hello" 20 | } 21 | 22 | let myInstance = MyClass() 23 | Mirror.reflectProperties(of: myInstance, matchingType: Int.self) { property in 24 | print("The value of myIntProperty is (property)") 25 | } 26 | ``` 27 | */ 28 | public static func reflectProperties( 29 | of target: Any, 30 | matchingType type: T.Type = T.self, 31 | recursively: Bool = false, 32 | using closure: (T) -> Void 33 | ) { 34 | let mirror = Mirror(reflecting: target) 35 | 36 | for child in mirror.children { 37 | (child.value as? T).map(closure) 38 | guard recursively else { continue } 39 | 40 | Mirror.reflectProperties( 41 | of: child.value, 42 | recursively: true, 43 | using: closure 44 | ) 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Sources/SwiftBoost/Foundation/Extensions/FileManager/FileManagerExtension.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension FileManager { 4 | 5 | public func fileExist(at destination: FileManagerDestination) -> Bool { 6 | return fileExists(atPath: destination.url.path) 7 | } 8 | 9 | public func folderExist(for destination: FileManagerDestination) -> Bool { 10 | return fileExists(atPath: destination.directory.path) 11 | } 12 | 13 | public func get(from destination: FileManagerDestination) -> Data? { 14 | do { 15 | return try Data(contentsOf: destination.url) 16 | } catch { 17 | debug("Can't get data, error: \(error.localizedDescription)") 18 | return nil 19 | } 20 | } 21 | 22 | public func save(data: Data, to destination: FileManagerDestination) { 23 | do { 24 | // If not exist directory, create it 25 | if !fileExists(atPath: destination.directory.path) { 26 | try createDirectory(at: destination.directory, withIntermediateDirectories: true, attributes: nil) 27 | } 28 | // Save file to directory 29 | try data.write(to: destination.url, options: .atomicWrite) 30 | } catch { 31 | debug("Can't save data, error: \(error.localizedDescription)") 32 | } 33 | } 34 | 35 | public func delete(at destination: FileManagerDestination) { 36 | do { 37 | if fileExists(atPath: destination.url.path) { 38 | try removeItem(at: destination.url) 39 | } 40 | } catch { 41 | debug("Can't delete data, error: \(error.localizedDescription)") 42 | } 43 | } 44 | 45 | public func delete(at url: URL) { 46 | do { 47 | if fileExists(atPath: url.path) { 48 | try removeItem(at: url) 49 | } 50 | } catch { 51 | debug("Can't delete data, error: \(error.localizedDescription)") 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Sources/SwiftBoost/UIKit/Extensions/UIApplicationExtension.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) && (os(iOS) || os(tvOS)) 2 | import UIKit 3 | 4 | public let kCFBundleDisplayNameKey = "CFBundleDisplayName" 5 | public let kCFBundleShortVersionStringKey = "CFBundleShortVersionString" 6 | 7 | public extension UIApplication { 8 | 9 | var bundleIdentifier: String? { Bundle.main.bundleIdentifier } 10 | var displayName: String? { Bundle.main.object(forInfoDictionaryKey: kCFBundleDisplayNameKey) as? String } 11 | var version: String? { Bundle.main.object(forInfoDictionaryKey: kCFBundleShortVersionStringKey) as? String } 12 | var buildNumber: String? { Bundle.main.object(forInfoDictionaryKey: kCFBundleVersionKey as String) as? String } 13 | 14 | func openSettings() { 15 | DispatchQueue.main.async { 16 | guard let settingsUrl = URL(string: UIApplication.openSettingsURLString) else { return } 17 | if UIApplication.shared.canOpenURL(settingsUrl) { 18 | UIApplication.shared.open(settingsUrl, completionHandler: { _ in }) 19 | } 20 | } 21 | } 22 | 23 | var rootController: UIViewController? { 24 | guard let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene else { return nil } 25 | guard let rootViewController = scene.windows.first?.rootViewController else { return nil } 26 | return rootViewController 27 | } 28 | 29 | var topController: UIViewController? { 30 | if var topController = self.rootController { 31 | while let presentedViewController = topController.presentedViewController { 32 | topController = presentedViewController 33 | } 34 | return topController 35 | } 36 | return nil 37 | } 38 | 39 | func openMailTo(_ email: String, subject: String, body: String = .empty) { 40 | let coded = "mailto:\(email)?subject=\(subject)&body=\(body)".addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) 41 | if let coded, let emailURL = URL(string: coded), canOpenURL(emailURL) { 42 | open(emailURL) 43 | } 44 | } 45 | } 46 | #endif 47 | -------------------------------------------------------------------------------- /Sources/SwiftBoost/UIKit/Extensions/UICollectionViewExtension.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) && (os(iOS) || os(tvOS)) 2 | import UIKit 3 | 4 | public extension UICollectionView { 5 | 6 | func isValidIndexPath(_ indexPath: IndexPath) -> Bool { 7 | return indexPath.section >= 0 && 8 | indexPath.item >= 0 && 9 | indexPath.section < numberOfSections && 10 | indexPath.item < numberOfItems(inSection: indexPath.section) 11 | } 12 | 13 | func safeScrollToItem(at indexPath: IndexPath, at scrollPosition: UICollectionView.ScrollPosition, animated: Bool) { 14 | guard isValidIndexPath(indexPath) else { return } 15 | scrollToItem(at: indexPath, at: scrollPosition, animated: animated) 16 | } 17 | 18 | // MARK: - Cell Registration 19 | 20 | func register(_ class: T.Type) { 21 | register(T.self, forCellWithReuseIdentifier: String(describing: `class`)) 22 | } 23 | 24 | func dequeueReusableCell(withClass class: T.Type, for indexPath: IndexPath) -> T { 25 | guard let cell = dequeueReusableCell(withReuseIdentifier: String(describing: `class`), for: indexPath) as? T else { 26 | fatalError() 27 | } 28 | return cell 29 | } 30 | 31 | // MARK: - Header & Footer Registration 32 | 33 | func register(_ class: T.Type, kind: String) { 34 | register(T.self, forSupplementaryViewOfKind: kind, withReuseIdentifier: String(describing: `class`)) 35 | } 36 | 37 | func dequeueReusableSupplementaryView(withCalss class: T.Type, kind: String, for indexPath: IndexPath) -> T { 38 | guard let view = dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: String(describing: `class`), for: indexPath) as? T else { 39 | fatalError() 40 | } 41 | return view 42 | } 43 | 44 | // MARK: - Layout 45 | 46 | func invalidateLayout(animated: Bool) { 47 | if animated { 48 | performBatchUpdates({ 49 | self.collectionViewLayout.invalidateLayout() 50 | }, completion: nil) 51 | } else { 52 | collectionViewLayout.invalidateLayout() 53 | } 54 | } 55 | } 56 | #endif 57 | -------------------------------------------------------------------------------- /Sources/SwiftBoost/UIKit/Extensions/UITabBarExtension.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) && (os(iOS) || os(tvOS)) 2 | import UIKit 3 | 4 | extension UITabBar { 5 | 6 | /** 7 | SwiftBoost: Set appearance for tab bar. 8 | */ 9 | @available(iOS 13.0, tvOS 13.0, *) 10 | public func setAppearance(_ value: TabBarAppearance) { 11 | self.standardAppearance = value.standardAppearance 12 | if #available(iOS 15.0, tvOS 15.0, *) { 13 | self.scrollEdgeAppearance = value.scrollEdgeAppearance 14 | } 15 | } 16 | 17 | /** 18 | SwiftBoost: Appearance cases. 19 | */ 20 | @available(iOS 13.0, tvOS 13.0, *) 21 | public enum TabBarAppearance { 22 | 23 | case transparentAlways 24 | case transparentStandardOnly 25 | case opaqueAlways 26 | 27 | public var standardAppearance: UITabBarAppearance { 28 | switch self { 29 | case .transparentAlways: 30 | let appearance = UITabBarAppearance() 31 | appearance.configureWithTransparentBackground() 32 | return appearance 33 | case .transparentStandardOnly: 34 | let appearance = UITabBarAppearance() 35 | appearance.configureWithDefaultBackground() 36 | return appearance 37 | case .opaqueAlways: 38 | let appearance = UITabBarAppearance() 39 | appearance.configureWithDefaultBackground() 40 | return appearance 41 | } 42 | } 43 | 44 | public var scrollEdgeAppearance: UITabBarAppearance { 45 | switch self { 46 | case .transparentAlways: 47 | let appearance = UITabBarAppearance() 48 | appearance.configureWithTransparentBackground() 49 | return appearance 50 | case .transparentStandardOnly: 51 | let appearance = UITabBarAppearance() 52 | appearance.configureWithTransparentBackground() 53 | return appearance 54 | case .opaqueAlways: 55 | let appearance = UITabBarAppearance() 56 | appearance.configureWithDefaultBackground() 57 | return appearance 58 | } 59 | } 60 | } 61 | } 62 | #endif 63 | -------------------------------------------------------------------------------- /Sources/SwiftBoost/UIKit/Extensions/UIToolbarExtension.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) && os(iOS) 2 | import UIKit 3 | 4 | extension UIToolbar { 5 | 6 | /** 7 | SwiftBoost: Set appearance for tab bar. 8 | */ 9 | @available(iOS 13.0, tvOS 13.0, *) 10 | public func setAppearance(_ value: ToolbarAppearance) { 11 | self.standardAppearance = value.standardAppearance 12 | if #available(iOS 15.0, tvOS 15.0, *) { 13 | self.scrollEdgeAppearance = value.scrollEdgeAppearance 14 | } 15 | } 16 | 17 | /** 18 | SwiftBoost: Appearance cases. 19 | */ 20 | @available(iOS 13.0, tvOS 13.0, *) 21 | public enum ToolbarAppearance { 22 | 23 | case transparentAlways 24 | case transparentStandardOnly 25 | case opaqueAlways 26 | 27 | public var standardAppearance: UIToolbarAppearance { 28 | switch self { 29 | case .transparentAlways: 30 | let appearance = UIToolbarAppearance() 31 | appearance.configureWithTransparentBackground() 32 | return appearance 33 | case .transparentStandardOnly: 34 | let appearance = UIToolbarAppearance() 35 | appearance.configureWithDefaultBackground() 36 | return appearance 37 | case .opaqueAlways: 38 | let appearance = UIToolbarAppearance() 39 | appearance.configureWithDefaultBackground() 40 | return appearance 41 | } 42 | } 43 | 44 | public var scrollEdgeAppearance: UIToolbarAppearance { 45 | switch self { 46 | case .transparentAlways: 47 | let appearance = UIToolbarAppearance() 48 | appearance.configureWithTransparentBackground() 49 | return appearance 50 | case .transparentStandardOnly: 51 | let appearance = UIToolbarAppearance() 52 | appearance.configureWithTransparentBackground() 53 | return appearance 54 | case .opaqueAlways: 55 | let appearance = UIToolbarAppearance() 56 | appearance.configureWithDefaultBackground() 57 | return appearance 58 | } 59 | } 60 | } 61 | } 62 | #endif 63 | -------------------------------------------------------------------------------- /Sources/SwiftBoost/Foundation/Extensions/StringExtension.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public extension String { 4 | 5 | static var empty: String { return "" } 6 | static var space: String { return " " } 7 | static var dot: String { return "." } 8 | static var newline: String { return "\n" } 9 | 10 | var words: [String] { 11 | return components(separatedBy: .punctuationCharacters).joined().components(separatedBy: .whitespaces) 12 | } 13 | 14 | var isEmail: Bool { 15 | let regex = "^(?:[\\p{L}0-9!#$%\\&'*+/=?\\^_`{|}~-]+(?:\\.[\\p{L}0-9!#$%\\&'*+/=?\\^_`{|}~-]+)*|\"(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21\\x23-\\x5b\\x5d-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])*\")@(?:(?:[\\p{L}0-9](?:[a-z0-9-]*[\\p{L}0-9])?\\.)+[\\p{L}0-9](?:[\\p{L}0-9-]*[\\p{L}0-9])?|\\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[\\p{L}0-9-]*[\\p{L}0-9]:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21-\\x5a\\x53-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])+)\\])$" 16 | return range(of: regex, options: .regularExpression, range: nil, locale: nil) != nil 17 | } 18 | 19 | var url: URL? { URL(string: self) } 20 | var isURL: Bool { self.url != nil} 21 | 22 | var bool: Bool? { 23 | let selfLowercased = trimmingCharacters(in: .whitespacesAndNewlines).lowercased() 24 | switch selfLowercased { 25 | case "true", "yes", "1": 26 | return true 27 | case "false", "no", "0": 28 | return false 29 | default: 30 | return nil 31 | } 32 | } 33 | 34 | var trim: String { trimmingCharacters(in: .whitespacesAndNewlines) } 35 | var isEmptyContent: Bool { trim == .empty } 36 | 37 | // MARK: - Edit 38 | 39 | func uppercasedFirstLetter() -> String { 40 | let lowercaseSctring = self.lowercased() 41 | return lowercaseSctring.prefix(1).uppercased() + lowercaseSctring.dropFirst() 42 | } 43 | 44 | func removedSuffix(_ suffix: String) -> String { 45 | if self.hasSuffix(suffix) { 46 | return String(dropLast(suffix.count)) 47 | } else { 48 | return self 49 | } 50 | } 51 | 52 | func removedPrefix(_ prefix: String) -> String { 53 | if self.hasPrefix(prefix) { 54 | return String(dropFirst(prefix.count)) 55 | } else { 56 | return self 57 | } 58 | } 59 | 60 | func replace(_ replacingString: String, with newString: String) -> String { 61 | return self.replacingOccurrences(of: replacingString, with: newString) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Sources/SwiftBoost/UIKit/Extensions/UINavigationBarExtension.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) && (os(iOS) || os(tvOS)) 2 | import UIKit 3 | 4 | public extension UINavigationBar { 5 | 6 | func setTitleFont(_ font: UIFont) { 7 | titleTextAttributes = [.font: font] 8 | } 9 | 10 | func setTitleColor(_ color: UIColor) { 11 | titleTextAttributes = [.foregroundColor: color] 12 | } 13 | 14 | func setColors(backgroundColor: UIColor, textColor: UIColor) { 15 | isTranslucent = false 16 | self.backgroundColor = backgroundColor 17 | barTintColor = backgroundColor 18 | setBackgroundImage(UIImage(), for: .default) 19 | tintColor = textColor 20 | titleTextAttributes = [.foregroundColor: textColor] 21 | } 22 | 23 | /** 24 | SwiftBoost: Set appearance for navigation bar. 25 | */ 26 | @available(iOS 13.0, tvOS 13.0, *) 27 | func setAppearance(_ value: NavigationBarAppearance) { 28 | self.standardAppearance = value.standardAppearance 29 | self.scrollEdgeAppearance = value.scrollEdgeAppearance 30 | } 31 | 32 | /** 33 | SwiftBoost: Appearance cases. 34 | */ 35 | @available(iOS 13.0, tvOS 13.0, *) 36 | enum NavigationBarAppearance { 37 | 38 | case transparentAlways 39 | case transparentStandardOnly 40 | case opaqueAlways 41 | 42 | var standardAppearance: UINavigationBarAppearance { 43 | switch self { 44 | case .transparentAlways: 45 | let appearance = UINavigationBarAppearance() 46 | appearance.configureWithTransparentBackground() 47 | return appearance 48 | case .transparentStandardOnly: 49 | let appearance = UINavigationBarAppearance() 50 | appearance.configureWithDefaultBackground() 51 | return appearance 52 | case .opaqueAlways: 53 | let appearance = UINavigationBarAppearance() 54 | appearance.configureWithDefaultBackground() 55 | return appearance 56 | } 57 | } 58 | 59 | var scrollEdgeAppearance: UINavigationBarAppearance { 60 | switch self { 61 | case .transparentAlways: 62 | let appearance = UINavigationBarAppearance() 63 | appearance.configureWithTransparentBackground() 64 | return appearance 65 | case .transparentStandardOnly: 66 | let appearance = UINavigationBarAppearance() 67 | appearance.configureWithTransparentBackground() 68 | return appearance 69 | case .opaqueAlways: 70 | let appearance = UINavigationBarAppearance() 71 | appearance.configureWithDefaultBackground() 72 | return appearance 73 | } 74 | } 75 | } 76 | } 77 | #endif 78 | -------------------------------------------------------------------------------- /Sources/SwiftBoost/Foundation/Logger.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /** 4 | SwiftBoost: Help to manage prints in console during your app running. 5 | You can set when levels you want see with configure. 6 | 7 | - important: 8 | Requerid before call `configure()` method with list of allowed levels. 9 | */ 10 | public enum Logger { 11 | 12 | public static func configure(levels: [Level] = Level.allCases, fileNameMode: FileNameMode = .show) { 13 | Configurator.shared.levels = levels 14 | Configurator.shared.fileNameMode = fileNameMode 15 | } 16 | 17 | public static func log(_ level: Level, message: LogMessage, filePath: String) { 18 | if Configurator.shared.levels.contains(level) { 19 | 20 | // Formatting text. 21 | var formattedMessage = message 22 | formattedMessage = formattedMessage.removedSuffix(String.dot) 23 | 24 | // Adding filename if need. 25 | switch Configurator.shared.fileNameMode { 26 | case .show: 27 | if let fileName = URL(string: filePath)?.lastPathComponent { 28 | formattedMessage += " [\(fileName)]" 29 | } else { 30 | formattedMessage += " [\(filePath)]" 31 | } 32 | case .hide: 33 | break 34 | } 35 | 36 | print(formattedMessage) 37 | } 38 | } 39 | 40 | // MARK: - Classes 41 | 42 | public typealias LogMessage = String 43 | 44 | /** 45 | SwiftBoost: Available levels for logging. 46 | 47 | Use `httpResponse` for response of API requests. 48 | 49 | Use `error` for critical bugs. 50 | 51 | Use `debug` for develop process. 52 | */ 53 | public enum Level: String, CaseIterable { 54 | 55 | case httpResponse 56 | case error 57 | case debug 58 | 59 | public var description: String { 60 | switch self { 61 | case .httpResponse: return "HTTP Response" 62 | case .error: return "Error" 63 | case .debug: return "Debug" 64 | } 65 | } 66 | } 67 | 68 | public enum FileNameMode { 69 | 70 | case show 71 | case hide 72 | } 73 | 74 | // MARK: - Sigltone 75 | 76 | private struct Configurator { 77 | 78 | var levels: [Level] = [] 79 | var fileNameMode: FileNameMode = .show 80 | 81 | static var shared = Configurator() 82 | private init() {} 83 | } 84 | } 85 | 86 | // MARK: - Public Functions 87 | 88 | public func error(_ message: Logger.LogMessage, filePath: String = #fileID) { 89 | Logger.log(.error, message: message, filePath: filePath) 90 | } 91 | 92 | public func debug(_ message: Logger.LogMessage, filePath: String = #fileID) { 93 | Logger.log(.debug, message: message, filePath: filePath) 94 | } 95 | -------------------------------------------------------------------------------- /Sources/SwiftBoost/Foundation/Extensions/URLSessionExtension.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public extension URLSession { 4 | 5 | enum AppError: Error { 6 | 7 | case invalidURL(String) 8 | case networkError(Error) 9 | case noResponse 10 | case decodingError(Error) 11 | 12 | public func errorMessage() -> String { 13 | switch self { 14 | case .invalidURL(let str): 15 | return "bad URL: \(str)" 16 | case .networkError(let error): 17 | return "network Error: \(error)" 18 | case .noResponse: 19 | return "no network response" 20 | case .decodingError(let error): 21 | return "decoding error: \(error)" 22 | } 23 | } 24 | } 25 | 26 | enum HTTPMethod { 27 | 28 | case get 29 | case post 30 | case put 31 | case delete 32 | 33 | var id: String { 34 | switch self { 35 | case .get: return "get" 36 | case .post: return "post" 37 | case .put: return "put" 38 | case .delete : return "delete" 39 | } 40 | } 41 | } 42 | 43 | enum ContentType { 44 | 45 | case application_json 46 | 47 | var id: String { 48 | switch self { 49 | case .application_json: 50 | "application/json" 51 | } 52 | } 53 | } 54 | 55 | static func request( 56 | url: String, 57 | method: HTTPMethod, 58 | body: [String: Any]? = nil, 59 | contentTypeHeader: ContentType? = nil, 60 | completion: @escaping (AppError?, Data?, HTTPURLResponse?) ->Void) 61 | { 62 | guard let url = URL(string: url) else { 63 | completion(AppError.invalidURL(url), nil, nil) 64 | return 65 | } 66 | 67 | var request = URLRequest(url: url) 68 | request.httpMethod = method.id 69 | 70 | // Content Type 71 | if let contentTypeHeader { 72 | request.setValue(contentTypeHeader.id, forHTTPHeaderField: "Content-Type") 73 | } 74 | 75 | // Body 76 | if let body { 77 | do { 78 | let jsonData = try JSONSerialization.data(withJSONObject: body, options: []) 79 | request.httpBody = jsonData 80 | } catch { 81 | completion(.decodingError(error), nil, nil) 82 | return 83 | } 84 | } 85 | 86 | // Make Request 87 | URLSession.shared.dataTask(with: request) { (data, response, error) in 88 | guard let response = response as? HTTPURLResponse else { 89 | completion(AppError.noResponse, nil, nil) 90 | return 91 | } 92 | 93 | if let error = error { 94 | completion(AppError.networkError(error), nil, response) 95 | } else if let data = data { 96 | completion(nil, data, response) 97 | } 98 | }.resume() 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /Sources/SwiftBoost/UIKit/Extensions/UIImageExtension.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) 2 | import UIKit 3 | 4 | public extension UIImage { 5 | 6 | // MARK: - Init 7 | 8 | convenience init(color: UIColor, size: CGSize) { 9 | UIGraphicsBeginImageContextWithOptions(size, false, 1) 10 | defer { 11 | UIGraphicsEndImageContext() 12 | } 13 | color.setFill() 14 | UIRectFill(CGRect(origin: .zero, size: size)) 15 | guard let aCgImage = UIGraphicsGetImageFromCurrentImageContext()?.cgImage else { 16 | self.init() 17 | return 18 | } 19 | self.init(cgImage: aCgImage) 20 | } 21 | 22 | static func system(_ name: String) -> UIImage { 23 | return UIImage.init(systemName: name) ?? UIImage() 24 | } 25 | 26 | static func system(_ name: String, pointSize: CGFloat, weight: UIImage.SymbolWeight) -> UIImage { 27 | let configuration = UIImage.SymbolConfiguration(pointSize: pointSize, weight: weight) 28 | return UIImage(systemName: name, withConfiguration: configuration) ?? UIImage() 29 | } 30 | 31 | static func system(_ name: String, font: UIFont) -> UIImage { 32 | let configuration = UIImage.SymbolConfiguration(font: font) 33 | return UIImage(systemName: name, withConfiguration: configuration) ?? UIImage() 34 | } 35 | 36 | // MARK: - Helpers 37 | 38 | var bytesSize: Int { jpegData(compressionQuality: 1)?.count ?? .zero } 39 | var kilobytesSize: Int { (jpegData(compressionQuality: 1)?.count ?? .zero) / 1024 } 40 | 41 | func compresse(quality: CGFloat) -> UIImage? { 42 | guard let data = jpegData(compressionQuality: quality) else { return nil } 43 | return UIImage(data: data) 44 | } 45 | 46 | func compressedData(quality: CGFloat) -> Data? { 47 | return jpegData(compressionQuality: quality) 48 | } 49 | 50 | // MARK: - Appearance 51 | 52 | var alwaysTemplate: UIImage { 53 | return withRenderingMode(.alwaysTemplate) 54 | } 55 | 56 | var alwaysOriginal: UIImage { 57 | return withRenderingMode(.alwaysOriginal) 58 | } 59 | 60 | func alwaysOriginal(with color: UIColor) -> UIImage { 61 | return withTintColor(color, renderingMode: .alwaysOriginal) 62 | } 63 | 64 | #if canImport(CoreImage) 65 | func resize(newWidth desiredWidth: CGFloat) -> UIImage { 66 | let oldWidth = size.width 67 | let scaleFactor = desiredWidth / oldWidth 68 | let newHeight = size.height * scaleFactor 69 | let newWidth = oldWidth * scaleFactor 70 | let newSize = CGSize(width: newWidth, height: newHeight) 71 | return resize(targetSize: newSize) 72 | } 73 | 74 | func resize(newHeight desiredHeight: CGFloat) -> UIImage { 75 | let scaleFactor = desiredHeight / size.height 76 | let newWidth = size.width * scaleFactor 77 | let newSize = CGSize(width: newWidth, height: desiredHeight) 78 | return resize(targetSize: newSize) 79 | } 80 | 81 | func resize(targetSize: CGSize) -> UIImage { 82 | return UIGraphicsImageRenderer(size:targetSize).image { _ in 83 | self.draw(in: CGRect(origin: .zero, size: targetSize)) 84 | } 85 | } 86 | #endif 87 | } 88 | #endif 89 | -------------------------------------------------------------------------------- /Sources/SwiftBoost/UIKit/Extensions/UIAlertControllerExtension.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) && (os(iOS) || os(tvOS)) 2 | import UIKit 3 | 4 | public extension UIAlertController { 5 | 6 | convenience init(title: String, message: String? = nil, actionButtonTitle: String) { 7 | self.init(title: title, message: message, preferredStyle: .alert) 8 | let defaultAction = UIAlertAction(title: actionButtonTitle, style: .default, handler: nil) 9 | addAction(defaultAction) 10 | } 11 | 12 | convenience init(title: String, error: Error, actionButtonTitle: String) { 13 | self.init(title: title, message: error.localizedDescription, preferredStyle: .alert) 14 | let defaultAction = UIAlertAction(title: actionButtonTitle, style: .default, handler: nil) 15 | addAction(defaultAction) 16 | } 17 | 18 | @discardableResult func addAction(title: String, style: UIAlertAction.Style = .default, handler: ((UIAlertAction) -> Void)? = nil) -> UIAlertAction { 19 | let action = UIAlertAction(title: title, style: style, handler: handler) 20 | addAction(action) 21 | return action 22 | } 23 | 24 | func addTextField(text: String? = nil, placeholder: String? = nil, editingChangedTarget: Any?, editingChangedSelector: Selector?) { 25 | addTextField { textField in 26 | textField.text = text 27 | textField.placeholder = placeholder 28 | if let target = editingChangedTarget, let selector = editingChangedSelector { 29 | textField.addTarget(target, action: selector, for: .editingChanged) 30 | } 31 | } 32 | } 33 | 34 | @available(iOS 14, tvOS 14, *) 35 | func addTextField( 36 | text: String? = nil, 37 | placeholder: String? = nil, 38 | action: UIAction? 39 | ) { 40 | addTextField { textField in 41 | textField.text = text 42 | textField.placeholder = placeholder 43 | if let action = action { 44 | textField.addAction(action, for: .editingChanged) 45 | } 46 | } 47 | } 48 | 49 | static func confirm(title: String, description: String, actionTitle: String, cancelTitle: String, desctructive: Bool, completion: @escaping (_ confirmed: Bool)->Void, sourceView: UIView, on controller: UIViewController) { 50 | let alertController = UIAlertController.init(title: title, message: description, preferredStyle: .actionSheet) 51 | alertController.popoverPresentationController?.sourceView = sourceView 52 | alertController.addAction(title: actionTitle, style: desctructive ? .destructive : .default) { [] _ in 53 | completion(true) 54 | } 55 | alertController.addAction(title: cancelTitle, style: .cancel, handler: { _ in 56 | completion(false) 57 | }) 58 | controller.present(alertController) 59 | } 60 | 61 | static func confirmDouble(title: String, description: String, actionTitle: String, cancelTitle: String, desctructive: Bool, completion: @escaping (_ confirmed: Bool)->Void, sourceView: UIView, on controller: UIViewController) { 62 | confirm(title: title, description: description, actionTitle: actionTitle, cancelTitle: cancelTitle, desctructive: desctructive, completion: { confirmed in 63 | if confirmed { 64 | confirm(title: title, description: description, actionTitle: actionTitle, cancelTitle: cancelTitle, desctructive: desctructive, completion: completion, sourceView: sourceView, on: controller) 65 | } else { 66 | completion(false) 67 | } 68 | }, sourceView: sourceView, on: controller) 69 | } 70 | } 71 | #endif 72 | -------------------------------------------------------------------------------- /Sources/SwiftBoost/UIKit/Extensions/UIViewControllerExtension.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) && (os(iOS) || os(tvOS)) 2 | import UIKit 3 | 4 | public extension UIViewController { 5 | 6 | // MARK: - Layout 7 | 8 | var systemSafeAreaInsets: UIEdgeInsets { 9 | return UIEdgeInsets( 10 | top: view.safeAreaInsets.top - additionalSafeAreaInsets.top, 11 | left: view.safeAreaInsets.left - additionalSafeAreaInsets.left, 12 | bottom: view.safeAreaInsets.bottom - additionalSafeAreaInsets.bottom, 13 | right: view.safeAreaInsets.right - additionalSafeAreaInsets.right 14 | ) 15 | } 16 | 17 | // MARK: - Containers 18 | 19 | func addChildWithView(_ childController: UIViewController, to containerView: UIView) { 20 | childController.willMove(toParent: self) 21 | addChild(childController) 22 | switch childController { 23 | case let collectionController as UICollectionViewController: 24 | containerView.addSubview(collectionController.collectionView) 25 | case let tableController as UITableViewController: 26 | containerView.addSubview(tableController.tableView) 27 | default: 28 | containerView.addSubview(childController.view) 29 | } 30 | childController.didMove(toParent: self) 31 | } 32 | 33 | // MARK: - Present, Dismiss and Destruct 34 | 35 | func present(_ viewControllerToPresent: UIViewController, completion: (ClosureVoid)? = nil) { 36 | self.present(viewControllerToPresent, animated: true, completion: completion) 37 | } 38 | 39 | @available(iOSApplicationExtension, unavailable) 40 | @available(tvOSApplicationExtension, unavailable) 41 | func destruct(scene name: String) { 42 | guard let session = view.window?.windowScene?.session else { 43 | dismissAnimated() 44 | return 45 | } 46 | if session.configuration.name == name { 47 | UIApplication.shared.requestSceneSessionDestruction(session, options: nil) 48 | } else { 49 | dismissAnimated() 50 | } 51 | } 52 | 53 | @objc func dismissAnimated() { 54 | dismiss(animated: true, completion: nil) 55 | } 56 | 57 | // MARK: - Bar Button Items 58 | 59 | #if os(iOS) 60 | var closeBarButtonItem: UIBarButtonItem { 61 | if #available(iOS 14.0, *) { 62 | return UIBarButtonItem.init(systemItem: .close, primaryAction: .init(handler: { [weak self] (action) in 63 | self?.dismissAnimated() 64 | }), menu: nil) 65 | } else { 66 | return UIBarButtonItem.init(barButtonSystemItem: .close, target: self, action: #selector(self.dismissAnimated)) 67 | } 68 | } 69 | 70 | @available(iOS 14, *) 71 | @available(iOSApplicationExtension, unavailable) 72 | func closeBarButtonItem(sceneName: String? = nil) -> UIBarButtonItem { 73 | return UIBarButtonItem.init(systemItem: .close, primaryAction: .init(handler: { [weak self] (action) in 74 | guard let self = self else { return } 75 | if let name = sceneName { 76 | self.destruct(scene: name) 77 | } else { 78 | self.dismissAnimated() 79 | } 80 | }), menu: nil) 81 | } 82 | #endif 83 | 84 | // MARK: - Keyboard 85 | 86 | func dismissKeyboardWhenTappedAround() { 87 | let tap = UITapGestureRecognizer(target: self, action: #selector(UIViewController.dismissKeyboardTappedAround(_:))) 88 | tap.cancelsTouchesInView = false 89 | view.addGestureRecognizer(tap) 90 | } 91 | 92 | @objc func dismissKeyboardTappedAround(_ gestureRecognizer: UIPanGestureRecognizer) { 93 | dismissKeyboard() 94 | } 95 | 96 | func dismissKeyboard() { 97 | view.endEditing(true) 98 | } 99 | } 100 | #endif 101 | 102 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | hello@ivanvorobei.io. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /Sources/SwiftBoost/UIKit/Extensions/UIColorExtension.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) 2 | import UIKit 3 | 4 | public extension UIColor { 5 | 6 | // MARK: - Init 7 | 8 | #if !os(watchOS) 9 | convenience init(light: UIColor, dark: UIColor) { 10 | if #available(iOS 13.0, tvOS 13.0, *) { 11 | self.init(dynamicProvider: { trait in 12 | trait.userInterfaceStyle == .dark ? dark : light 13 | }) 14 | } else { 15 | self.init(cgColor: light.cgColor) 16 | } 17 | } 18 | #endif 19 | 20 | #if !os(watchOS) && !os(tvOS) 21 | convenience init(baseInterfaceLevel: UIColor, elevatedInterfaceLevel: UIColor ) { 22 | if #available(iOS 13.0, tvOS 13.0, *) { 23 | self.init { traitCollection in 24 | switch traitCollection.userInterfaceLevel { 25 | case .base: 26 | return baseInterfaceLevel 27 | case .elevated: 28 | return elevatedInterfaceLevel 29 | case .unspecified: 30 | return baseInterfaceLevel 31 | @unknown default: 32 | return baseInterfaceLevel 33 | } 34 | } 35 | } 36 | else { 37 | self.init(cgColor: baseInterfaceLevel.cgColor) 38 | } 39 | } 40 | #endif 41 | 42 | convenience init(hex: String) { 43 | let hex = UIColor.parseHex(hex: hex, alpha: nil) 44 | self.init(red: hex.red, green: hex.green, blue: hex.blue, alpha: hex.alpha) 45 | } 46 | 47 | convenience init(hex: String, alpha: CGFloat) { 48 | let hex = UIColor.parseHex(hex: hex, alpha: alpha) 49 | self.init(red: hex.red, green: hex.green, blue: hex.blue, alpha: hex.alpha) 50 | } 51 | 52 | // MARK: - Application 53 | 54 | #if !os(watchOS) 55 | @available(iOSApplicationExtension, unavailable) 56 | @available(tvOSApplicationExtension, unavailable) 57 | static var tint: UIColor { 58 | get { 59 | let value = UIApplication.shared.windows.first?.tintColor 60 | guard let tint = value else { return .systemBlue } 61 | return tint 62 | } 63 | set { 64 | UIApplication.shared.windows.forEach({ $0.tintColor = newValue }) 65 | } 66 | } 67 | #endif 68 | 69 | // MARK: - Helpers 70 | 71 | var hex: String { 72 | let colorRef = cgColor.components 73 | let r = colorRef?[0] ?? 0 74 | let g = colorRef?[1] ?? 0 75 | let b = ((colorRef?.count ?? 0) > 2 ? colorRef?[2] : g) ?? 0 76 | let a = cgColor.alpha 77 | 78 | var color = String( 79 | format: "#%02lX%02lX%02lX", 80 | lroundf(Float(r * 255)), 81 | lroundf(Float(g * 255)), 82 | lroundf(Float(b * 255)) 83 | ) 84 | 85 | if a < 1 { 86 | color += String(format: "%02lX", lroundf(Float(a))) 87 | } 88 | 89 | return color 90 | } 91 | 92 | func alpha(_ alpha: CGFloat) -> UIColor { 93 | self.withAlphaComponent(alpha) 94 | } 95 | 96 | #if !os(watchOS) 97 | var alpha: CGFloat { CIColor(color: self).alpha } 98 | #endif 99 | 100 | func lighter(by amount: CGFloat) -> UIColor { mixWithColor(UIColor.white, amount: amount) } 101 | func darker(by amount: CGFloat) -> UIColor { mixWithColor(UIColor.black, amount: amount) } 102 | 103 | func mixWithColor(_ color: UIColor, amount: CGFloat = 0.25) -> UIColor { 104 | var r1 : CGFloat = 0 105 | var g1 : CGFloat = 0 106 | var b1 : CGFloat = 0 107 | var alpha1 : CGFloat = 0 108 | var r2 : CGFloat = 0 109 | var g2 : CGFloat = 0 110 | var b2 : CGFloat = 0 111 | var alpha2 : CGFloat = 0 112 | 113 | self.getRed (&r1, green: &g1, blue: &b1, alpha: &alpha1) 114 | color.getRed(&r2, green: &g2, blue: &b2, alpha: &alpha2) 115 | return UIColor( 116 | red: r1 * (1.0 - amount) + r2 * amount, 117 | green: g1 * (1.0 - amount) + g2 * amount, 118 | blue: b1 * (1.0 - amount) + b2 * amount, 119 | alpha: alpha1) 120 | } 121 | 122 | private static func parseHex(hex: String, alpha: CGFloat?) -> (red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat) { 123 | var red: CGFloat = 0.0 124 | var green: CGFloat = 0.0 125 | var blue: CGFloat = 0.0 126 | var newAlpha: CGFloat = alpha ?? 1.0 127 | var hex: String = hex 128 | 129 | if hex.hasPrefix("#") { 130 | let index = hex.index(hex.startIndex, offsetBy: 1) 131 | hex = String(hex[index...]) 132 | } 133 | 134 | let scanner = Scanner(string: hex) 135 | var hexValue: CUnsignedLongLong = 0 136 | if scanner.scanHexInt64(&hexValue) { 137 | switch (hex.count) { 138 | case 3: 139 | red = CGFloat((hexValue & 0xF00) >> 8) / 15.0 140 | green = CGFloat((hexValue & 0x0F0) >> 4) / 15.0 141 | blue = CGFloat(hexValue & 0x00F) / 15.0 142 | case 4: 143 | red = CGFloat((hexValue & 0xF000) >> 12) / 15.0 144 | green = CGFloat((hexValue & 0x0F00) >> 8) / 15.0 145 | blue = CGFloat((hexValue & 0x00F0) >> 4) / 15.0 146 | if alpha == nil { 147 | newAlpha = CGFloat(hexValue & 0x000F) / 15.0 148 | } 149 | case 6: 150 | red = CGFloat((hexValue & 0xFF0000) >> 16) / 255.0 151 | green = CGFloat((hexValue & 0x00FF00) >> 8) / 255.0 152 | blue = CGFloat(hexValue & 0x0000FF) / 255.0 153 | case 8: 154 | red = CGFloat((hexValue & 0xFF000000) >> 24) / 255.0 155 | green = CGFloat((hexValue & 0x00FF0000) >> 16) / 255.0 156 | blue = CGFloat((hexValue & 0x0000FF00) >> 8) / 255.0 157 | if alpha == nil { 158 | newAlpha = CGFloat(hexValue & 0x000000FF) / 255.0 159 | } 160 | default: 161 | print("UIColorExtension - Invalid RGB string, number of characters after '#' should be either 3, 4, 6 or 8") 162 | } 163 | } else { 164 | print("UIColorExtension - Scan hex error") 165 | } 166 | return (red, green, blue, newAlpha) 167 | } 168 | 169 | // MARK: - Data 170 | 171 | #if !os(watchOS) 172 | static var systemColorfulColors: [UIColor] { 173 | if #available(iOS 13.0, tvOS 13.0, *) { 174 | return [.systemRed, .systemOrange, .systemYellow, .systemGreen, .systemTeal, .systemBlue, .systemIndigo, .systemPink, .systemPurple] 175 | } else { 176 | return [.systemRed, .systemOrange, .systemYellow, .systemGreen, .systemTeal, .systemBlue, .systemPink, .systemPurple] 177 | } 178 | } 179 | #endif 180 | 181 | #if !os(watchOS) 182 | var secondary: UIColor { 183 | return .init(light: self.alpha(0.06), dark: self.alpha(0.18)) 184 | } 185 | #endif 186 | 187 | #if !os(watchOS) && !os(tvOS) 188 | static var footnoteColor: UIColor { 189 | if #available(iOS 13.0, tvOS 13, *) { 190 | return UIColor.init { (trait) -> UIColor in 191 | return trait.userInterfaceStyle == .dark ? UIColor(hex: "8E8E93") : UIColor(hex: "6D6D72") 192 | } 193 | } else { 194 | return UIColor(hex: "6D6D72") 195 | } 196 | } 197 | #endif 198 | 199 | #if !os(watchOS) 200 | static var destructiveColor: UIColor { .systemRed } 201 | static var warningColor: UIColor { .systemOrange } 202 | #endif 203 | } 204 | #endif 205 | -------------------------------------------------------------------------------- /Sources/SwiftBoost/Foundation/Extensions/DateExtension.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public extension Date { 4 | 5 | static func - (lhs: Date, rhs: Date) -> TimeInterval { 6 | return lhs.timeIntervalSinceReferenceDate - rhs.timeIntervalSinceReferenceDate 7 | } 8 | 9 | #if (os(iOS) || os(tvOS)) 10 | static var current: Date { 11 | #if targetEnvironment(macCatalyst) 12 | return Date() 13 | #else 14 | if #available(macOS 13, iOS 15, tvOS 15, watchOS 8, *) { 15 | return Date.now 16 | } else { 17 | return Date() 18 | } 19 | #endif 20 | } 21 | #endif 22 | 23 | var isInFuture: Bool { self > Date() } 24 | var isInPast: Bool { self < Date() } 25 | var isInToday: Bool { Calendar.current.isDateInToday(self) } 26 | var isInYesterday: Bool { Calendar.current.isDateInYesterday(self) } 27 | var isInTomorrow: Bool { Calendar.current.isDateInTomorrow(self) } 28 | var isInWeekend: Bool { Calendar.current.isDateInWeekend(self) } 29 | var isWorkday: Bool { !Calendar.current.isDateInWeekend(self) } 30 | 31 | var isInCurrentWeek: Bool { 32 | Calendar.current.isDate(self, equalTo: Date(), toGranularity: .weekOfYear) 33 | } 34 | var isInCurrentMonth: Bool { 35 | Calendar.current.isDate(self, equalTo: Date(), toGranularity: .month) 36 | } 37 | var isInCurrentYear: Bool { 38 | Calendar.current.isDate(self, equalTo: Date(), toGranularity: .year) 39 | } 40 | 41 | func isBetween(_ startDate: Date, and endDate: Date) -> Bool { 42 | return (min(startDate, endDate) ... max(startDate, endDate)).contains(self) 43 | } 44 | 45 | func component(_ component: Calendar.Component) -> Int { 46 | Calendar.current.component(component, from: self) 47 | } 48 | 49 | /** 50 | SwiftBoost: Returns the difference between two dates 51 | 52 | Take a look at this example: 53 | ``` 54 | today.age(to: nextWeek, component: .day) // 7 55 | ``` 56 | 57 | - parameter date: Date until which to generate the array 58 | - parameter component: The step with which the array is generated 59 | */ 60 | func difference(to date: Date, component: Calendar.Component) -> Int { 61 | let components = Calendar.current.dateComponents([component], from: self, to: date) 62 | switch component { 63 | case .nanosecond: 64 | return components.nanosecond ?? 0 65 | case .second: 66 | return components.second ?? 0 67 | case .minute: 68 | return components.minute ?? 0 69 | case .hour: 70 | return components.hour ?? 0 71 | case .day: 72 | return components.day ?? 0 73 | case .month: 74 | return components.month ?? 0 75 | case .year: 76 | return components.year ?? 0 77 | default: 78 | return 0 79 | } 80 | } 81 | 82 | // MARK: - Edit 83 | 84 | func add(_ component: Calendar.Component, value: Int = 1) -> Date { 85 | return Calendar.current.date(byAdding: component, value: value, to: self) ?? self 86 | } 87 | 88 | mutating func add(_ component: Calendar.Component, value: Int = 1) { 89 | if let date = Calendar.current.date(byAdding: component, value: value, to: self) { 90 | self = date 91 | } 92 | } 93 | 94 | func previous(_ component: Calendar.Component) -> Date { 95 | add(component, value: -1) 96 | } 97 | 98 | func next(_ component: Calendar.Component) -> Date { 99 | add(component) 100 | } 101 | 102 | /** 103 | SwiftBoost: Returns the start of component. 104 | 105 | - important: If it was not possible to get the end of the component, then self is returned. 106 | - parameter component: The component you want to get the start of (year, month, day, etc.). 107 | - returns: The start of component. 108 | */ 109 | func start(of component: Calendar.Component) -> Date { 110 | if component == .day { 111 | return Calendar.current.startOfDay(for: self) 112 | } 113 | var components: Set { 114 | switch component { 115 | case .second: return [.year, .month, .day, .hour, .minute, .second] 116 | case .minute: return [.year, .month, .day, .hour, .minute] 117 | case .hour: return [.year, .month, .day, .hour] 118 | case .day: return [.year, .month, .day] 119 | case .weekOfYear, .weekOfMonth: return [.yearForWeekOfYear, .weekOfYear] 120 | case .month: return [.year, .month] 121 | case .year: return [.year] 122 | default: return [] 123 | } 124 | } 125 | guard components.isEmpty == false else { return self } 126 | return Calendar.current.date(from: Calendar.current.dateComponents(components, from: self)) ?? self 127 | } 128 | 129 | /** 130 | SwiftBoost: Returns the end of component. 131 | 132 | - important: If it was not possible to get the end of the component, then self is returned. 133 | - parameter component: The component you want to get the end of (year, month, day, etc.). 134 | - returns: The end of component. 135 | */ 136 | func end(of component: Calendar.Component) -> Date { 137 | let date = self.start(of: component) 138 | var components: DateComponents? { 139 | switch component { 140 | case .second: 141 | var components = DateComponents() 142 | components.second = 1 143 | components.nanosecond = -1 144 | return components 145 | case .minute: 146 | var components = DateComponents() 147 | components.minute = 1 148 | components.second = -1 149 | return components 150 | case .hour: 151 | var components = DateComponents() 152 | components.hour = 1 153 | components.second = -1 154 | return components 155 | case .day: 156 | var components = DateComponents() 157 | components.day = 1 158 | components.second = -1 159 | return components 160 | case .weekOfYear, .weekOfMonth: 161 | var components = DateComponents() 162 | components.weekOfYear = 1 163 | components.second = -1 164 | return components 165 | case .month: 166 | var components = DateComponents() 167 | components.month = 1 168 | components.second = -1 169 | return components 170 | case .year: 171 | var components = DateComponents() 172 | components.year = 1 173 | components.second = -1 174 | return components 175 | default: 176 | return nil 177 | } 178 | } 179 | guard let addedComponent = components else { return self } 180 | return Calendar.current.date(byAdding: addedComponent, to: date) ?? self 181 | } 182 | 183 | // MARK: - Formatting 184 | 185 | func formatted(dateStyle: DateFormatter.Style, timeStyle: DateFormatter.Style = .none) -> String { 186 | DateFormatter.localizedString(from: self, dateStyle: dateStyle, timeStyle: timeStyle) 187 | } 188 | 189 | /** 190 | SwiftBoost: Localise date by format. 191 | 192 | You can manually manage date formatted or set localised. 193 | 194 | Take a look at this example: 195 | ``` 196 | print(Date().formatted(localized: true)) // 05/16/2022, 14:17 197 | print(Date().formatted(localized: false)) // 16.05.2022 14:17 198 | ``` 199 | 200 | - parameter numberOfUnits: Count of units of component. 201 | - parameter unitsStyle: Style of formatting of units. 202 | */ 203 | func formatted(as format: String = "dd.MM.yyyy HH:mm", localized: Bool) -> String { 204 | let dateFormatter = DateFormatter() 205 | dateFormatter.locale = Locale.current 206 | if localized { 207 | dateFormatter.setLocalizedDateFormatFromTemplate(format) 208 | } else { 209 | dateFormatter.dateFormat = format 210 | } 211 | return dateFormatter.string(from: self) 212 | } 213 | 214 | func formattedInterval(to date: Date, dateStyle: DateIntervalFormatter.Style, timeStyle: DateIntervalFormatter.Style = .none) -> String { 215 | let formatter = DateIntervalFormatter() 216 | formatter.dateStyle = dateStyle 217 | formatter.timeStyle = timeStyle 218 | return formatter.string(from: self, to: date) 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /Sources/SwiftBoost/UIKit/Extensions/UIViewExtension.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) && (os(iOS) || os(tvOS)) 2 | import UIKit 3 | 4 | public extension UIView { 5 | 6 | convenience init(backgroundColor color: UIColor) { 7 | self.init() 8 | backgroundColor = color 9 | } 10 | 11 | var viewController: UIViewController? { 12 | weak var parentResponder: UIResponder? = self 13 | while parentResponder != nil { 14 | parentResponder = parentResponder!.next 15 | if let viewController = parentResponder as? UIViewController { 16 | return viewController 17 | } 18 | } 19 | return nil 20 | } 21 | 22 | func addSubviews(_ subviews: [UIView]) { subviews.forEach { addSubview($0) } } 23 | func addSubviews(_ subviews: UIView...) { subviews.forEach { addSubview($0) } } 24 | func removeSubviews() { subviews.forEach { $0.removeFromSuperview() } } 25 | 26 | var hasSuperview: Bool { superview != nil } 27 | 28 | func allSubViewsOf(type : T.Type) -> [T] { 29 | var all = [T]() 30 | func getSubview(view: UIView) { 31 | if let aView = view as? T{ 32 | all.append(aView) 33 | } 34 | guard view.subviews.count>0 else { return } 35 | view.subviews.forEach{ getSubview(view: $0) } 36 | } 37 | getSubview(view: self) 38 | return all 39 | } 40 | 41 | var screenshot: UIImage? { 42 | /*UIGraphicsBeginImageContextWithOptions(layer.frame.size, false, 0) 43 | defer { 44 | UIGraphicsEndImageContext() 45 | } 46 | guard let context = UIGraphicsGetCurrentContext() else { return nil } 47 | layer.render(in: context) 48 | return UIGraphicsGetImageFromCurrentImageContext()*/ 49 | let renderer = UIGraphicsImageRenderer(bounds: bounds) 50 | return renderer.image { rendererContext in 51 | layer.render(in: rendererContext.cgContext) 52 | } 53 | } 54 | 55 | // MARK: - Layout 56 | 57 | var ltr: Bool { effectiveUserInterfaceLayoutDirection == .leftToRight } 58 | var rtl: Bool { effectiveUserInterfaceLayoutDirection == .rightToLeft } 59 | 60 | func setEqualSuperviewBoundsWithFrames() { 61 | guard let superview = self.superview else { return } 62 | if frame != superview.bounds { 63 | frame = superview.bounds 64 | } 65 | } 66 | 67 | func setEqualSuperviewBoundsWithAutoresizingMask() { 68 | guard let superview = self.superview else { return } 69 | if frame != superview.bounds { 70 | frame = superview.bounds 71 | } 72 | autoresizingMask = [.flexibleWidth, .flexibleHeight] 73 | } 74 | 75 | func setEqualSuperviewBoundsWithAutoLayout() { 76 | guard let superview = self.superview else { return } 77 | translatesAutoresizingMaskIntoConstraints = false 78 | NSLayoutConstraint.activate([ 79 | topAnchor.constraint(equalTo: superview.topAnchor), 80 | leftAnchor.constraint(equalTo: superview.leftAnchor), 81 | rightAnchor.constraint(equalTo: superview.rightAnchor), 82 | bottomAnchor.constraint(equalTo: superview.bottomAnchor) 83 | ]) 84 | } 85 | 86 | func setEqualSuperviewMarginsWithFrames() { 87 | guard let superview = self.superview else { return } 88 | frame = .init(x: superview.layoutMargins.left, y: superview.layoutMargins.top, width: superview.layoutWidth, height: superview.layoutHeight) 89 | } 90 | 91 | func setEqualSuperviewMarginsWithAutoLayout() { 92 | guard let superview = self.superview else { return } 93 | translatesAutoresizingMaskIntoConstraints = false 94 | NSLayoutConstraint.activate([ 95 | topAnchor.constraint(equalTo: superview.layoutMarginsGuide.topAnchor), 96 | leftAnchor.constraint(equalTo: superview.layoutMarginsGuide.leftAnchor), 97 | rightAnchor.constraint(equalTo: superview.layoutMarginsGuide.rightAnchor), 98 | bottomAnchor.constraint(equalTo: superview.layoutMarginsGuide.bottomAnchor) 99 | ]) 100 | } 101 | 102 | 103 | func setWidthAndFit(width: CGFloat) { 104 | frame.setWidth(width) 105 | sizeToFit() 106 | } 107 | 108 | func setXToSuperviewLeftMargin() { 109 | guard let superview = self.superview else { return } 110 | frame.origin.x = superview.layoutMargins.left 111 | } 112 | 113 | func setMaxXToSuperviewRightMargin() { 114 | guard let superview = self.superview else { return } 115 | frame.setMaxX(superview.frame.width - superview.layoutMargins.right) 116 | } 117 | 118 | func setMaxYToSuperviewBottomMargin() { 119 | guard let superview = self.superview else { return } 120 | frame.setMaxY(superview.frame.height - superview.layoutMargins.bottom) 121 | } 122 | 123 | func setXCenter() { center.x = (superview?.frame.width ?? .zero) / 2 } 124 | func setYCenter() { center.y = (superview?.frame.height ?? .zero) / 2 } 125 | 126 | func setToCenter() { 127 | setXCenter() 128 | setYCenter() 129 | } 130 | 131 | // MARK: Readable Content Guide 132 | 133 | var readableMargins: UIEdgeInsets { 134 | let layoutFrame = readableContentGuide.layoutFrame 135 | return UIEdgeInsets( 136 | top: layoutFrame.origin.y, 137 | left: layoutFrame.origin.x, 138 | bottom: frame.height - layoutFrame.height - layoutFrame.origin.y, 139 | right: frame.width - layoutFrame.width - layoutFrame.origin.x 140 | ) 141 | } 142 | 143 | var readableWidth: CGFloat { readableContentGuide.layoutFrame.width } 144 | var readableHeight: CGFloat { readableContentGuide.layoutFrame.height } 145 | 146 | var readableFrame: CGRect { 147 | let margins = readableMargins 148 | return CGRect.init(x: margins.left, y: margins.top, width: readableWidth, height: readableHeight) 149 | } 150 | 151 | // MARK: Layout Margins Guide 152 | 153 | var layoutWidth: CGFloat { frame.width - layoutMargins.left - layoutMargins.right } 154 | var layoutHeight: CGFloat { frame.height - layoutMargins.top - layoutMargins.bottom } 155 | var layoutFrame: CGRect { CGRect.init(x: layoutMargins.left, y: layoutMargins.top, width: layoutWidth, height: layoutHeight) } 156 | 157 | func setSafeLayoutMargins(_ value: UIEdgeInsets) { 158 | if layoutMargins != value { 159 | layoutMargins = value 160 | } 161 | } 162 | 163 | // MARK: - Appearance 164 | 165 | var masksToBounds: Bool { 166 | get { layer.masksToBounds } 167 | set { layer.masksToBounds = newValue } 168 | } 169 | 170 | func roundCorners(_ corners: CACornerMask = [.layerMaxXMaxYCorner, .layerMaxXMinYCorner, .layerMinXMaxYCorner, .layerMinXMinYCorner], curve: CALayerCornerCurve, radius: CGFloat) { 171 | layer.cornerRadius = radius 172 | layer.maskedCorners = corners 173 | layer.cornerCurve = curve 174 | } 175 | 176 | func roundMinimumSide() { 177 | roundCorners(curve: .circular, radius: min(frame.width / 2, frame.height / 2)) 178 | } 179 | 180 | var borderColor: UIColor? { 181 | get { 182 | guard let color = layer.borderColor else { return nil } 183 | return UIColor(cgColor: color) 184 | } 185 | set { 186 | guard let color = newValue else { 187 | layer.borderColor = nil 188 | return 189 | } 190 | // Fix React-Native conflict issue 191 | guard String(describing: type(of: color)) != "__NSCFType" else { return } 192 | layer.borderColor = color.cgColor 193 | } 194 | } 195 | 196 | var borderWidth: CGFloat { 197 | get { layer.borderWidth } 198 | set { layer.borderWidth = newValue } 199 | } 200 | 201 | func addShadow(ofColor color: UIColor, radius: CGFloat, offset: CGSize, opacity: Float) { 202 | layer.shadowColor = color.cgColor 203 | layer.shadowOffset = offset 204 | layer.shadowRadius = radius 205 | layer.shadowOpacity = opacity 206 | layer.masksToBounds = false 207 | } 208 | 209 | func addParalax(amount: CGFloat) { 210 | motionEffects.removeAll() 211 | let horizontal = UIInterpolatingMotionEffect(keyPath: "center.x", type: .tiltAlongHorizontalAxis) 212 | horizontal.minimumRelativeValue = -amount 213 | horizontal.maximumRelativeValue = amount 214 | 215 | let vertical = UIInterpolatingMotionEffect(keyPath: "center.y", type: .tiltAlongVerticalAxis) 216 | vertical.minimumRelativeValue = -amount 217 | vertical.maximumRelativeValue = amount 218 | 219 | let group = UIMotionEffectGroup() 220 | group.motionEffects = [horizontal, vertical] 221 | self.addMotionEffect(group) 222 | } 223 | 224 | func removeParalax() { 225 | motionEffects.removeAll() 226 | } 227 | } 228 | #endif 229 | --------------------------------------------------------------------------------