├── Pod ├── Assets │ └── .gitkeep └── Classes │ ├── .gitkeep │ ├── HairlineView │ ├── HairlineTableCells.png │ ├── HairlineButtonDivide.png │ ├── README.md │ └── HairlineView.swift │ ├── TintedButton │ ├── simpleTintedButton.png │ ├── advancedTintedButton.png │ ├── README.md │ └── TintedButton.swift │ ├── Views │ ├── Gradient │ │ ├── GradientCustom.png │ │ ├── GradientSimple.png │ │ ├── README.md │ │ └── GradientView.swift │ └── Textview │ │ ├── TailorTextView.png │ │ ├── PlaceholderTextSimple.png │ │ ├── ExpandingTextView.swift │ │ ├── README.md │ │ ├── Protocols.swift │ │ ├── PlaceholderTextView.swift │ │ └── UITextView+Extensions.swift │ ├── Acknowledgements │ ├── AcknowledgementViewController.png │ ├── AcknowledgementsListViewController.png │ ├── CustomAcknowledgementViewController.png │ ├── CustomAcknowledgementsListViewController.png │ ├── AcknowledgementsListViewController.swift │ ├── AcknowledgementsListViewModel.swift │ ├── README.md │ └── AcknowledgementViewController.swift │ ├── Lifecycle │ ├── Behaviors │ │ ├── Nav-Bar-Hairline-Fade │ │ │ ├── example.gif │ │ │ └── readme.md │ │ └── Nav-Bar-Title-Transition │ │ │ ├── example.gif │ │ │ └── readme.md │ ├── Framework │ │ ├── UIViewController+Lifecycle.swift │ │ ├── ViewControllerLifecycleBehavior.swift │ │ ├── DefaultBehaviors.swift │ │ └── LifecycleBehaviorViewController.swift │ └── readme.md │ ├── Compatibility │ └── Compatibility.swift │ ├── TableViewHelpers │ ├── UITableView+Extensions.swift │ ├── TableSection.swift │ └── IndexPath+Extensions.swift │ ├── Math │ ├── Clamping.swift │ ├── FloatingPoint+Scale.swift │ ├── CurveProvider.swift │ └── CubicBezier.swift │ ├── StackViewHelpers │ └── UIStackView+Helpers.swift │ ├── DeviceSize │ ├── CGSize+DeviceSize.swift │ ├── CGFloat+DeviceSize.swift │ └── DeviceSize.swift │ ├── Logging │ ├── README.md │ └── Log.swift │ ├── BetterButton │ └── README.md │ ├── Forms │ └── UIView+Lookup.swift │ ├── ColorHelpers │ └── UIColor+Helpers.swift │ ├── RootViewController │ └── UIWindow+RootViewController.swift │ ├── AboutView │ ├── AppInfo.swift │ └── AboutView.swift │ ├── Deselection │ └── UIViewController+Deselection.swift │ ├── LicenseFormatter │ └── LicenseFormatter.swift │ ├── ImageHelpers │ └── UIImage+Tinting.swift │ ├── FormattedTextField │ └── FormattedTextField.swift │ ├── AccessibilityHelpers │ └── AccessibilityHelpers.swift │ └── Keyboard │ └── UIView+KeyboardLayoutGuide.swift ├── _Pods.xcodeproj ├── Gemfile ├── Example ├── Swiftilities │ ├── Assets │ │ ├── Images.xcassets │ │ │ ├── Contents.json │ │ │ ├── icn-twitter.imageset │ │ │ │ ├── 498-twitter.png │ │ │ │ ├── 498-twitter@2x.png │ │ │ │ ├── 498-twitter@3x.png │ │ │ │ └── Contents.json │ │ │ ├── logo-built-by-RZ.imageset │ │ │ │ ├── logo-built-by-RZ.png │ │ │ │ ├── logo-built-by-RZ@2x.png │ │ │ │ ├── logo-built-by-RZ@3x.png │ │ │ │ └── Contents.json │ │ │ └── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ └── Base.lproj │ │ │ └── LaunchScreen.xib │ ├── Views │ │ ├── Lifecycle │ │ │ ├── LogAppearanceBehavior.swift │ │ │ ├── LifecycleBehaviorsListViewController.swift │ │ │ ├── NavBarHairlineFadeDemoViewController.swift │ │ │ └── NavBarTitleTransitionDemoViewController.swift │ │ ├── Text Formatting │ │ │ └── TextFormattingViewController.swift │ │ ├── Keyboard Avoidance │ │ │ └── KeyboardAvoidanceViewController.swift │ │ ├── About View │ │ │ └── AboutViewController.swift │ │ ├── Tinted Buttons │ │ │ └── TintedButtonsViewController.swift │ │ ├── GradientViewController.swift │ │ ├── Main │ │ │ └── MainScreenViewController.swift │ │ └── ShapesViewController.swift │ ├── App Delegate │ │ └── AppDelegate.swift │ ├── Info.plist │ └── SupportingFiles │ │ └── TestModel.xcdatamodeld │ │ └── TestModel.xcdatamodel │ │ └── contents ├── Pods │ ├── Target Support Files │ │ ├── Pods-Swiftilities_Tests │ │ │ ├── Pods-Swiftilities_Tests-frameworks-Debug-output-files.xcfilelist │ │ │ ├── Pods-Swiftilities_Tests-frameworks-Release-output-files.xcfilelist │ │ │ ├── Pods-Swiftilities_Tests.modulemap │ │ │ ├── Pods-Swiftilities_Tests-dummy.m │ │ │ ├── Pods-Swiftilities_Tests-frameworks-Debug-input-files.xcfilelist │ │ │ ├── Pods-Swiftilities_Tests-frameworks-Release-input-files.xcfilelist │ │ │ ├── Pods-Swiftilities_Tests-umbrella.h │ │ │ ├── Pods-Swiftilities_Tests.debug.xcconfig │ │ │ ├── Pods-Swiftilities_Tests.release.xcconfig │ │ │ ├── Info.plist │ │ │ ├── Pods-Swiftilities_Tests-Info.plist │ │ │ ├── Pods-Swiftilities_Tests-acknowledgements.markdown │ │ │ └── Pods-Swiftilities_Tests-acknowledgements.plist │ │ ├── Pods-Swiftilities_Example │ │ │ ├── Pods-Swiftilities_Example-frameworks-Debug-output-files.xcfilelist │ │ │ ├── Pods-Swiftilities_Example-frameworks-Release-output-files.xcfilelist │ │ │ ├── Pods-Swiftilities_Example.modulemap │ │ │ ├── Pods-Swiftilities_Example-dummy.m │ │ │ ├── Pods-Swiftilities_Example-frameworks-Debug-input-files.xcfilelist │ │ │ ├── Pods-Swiftilities_Example-frameworks-Release-input-files.xcfilelist │ │ │ ├── Pods-Swiftilities_Example-umbrella.h │ │ │ ├── Pods-Swiftilities_Example.debug.xcconfig │ │ │ ├── Pods-Swiftilities_Example.release.xcconfig │ │ │ ├── Info.plist │ │ │ ├── Pods-Swiftilities_Example-Info.plist │ │ │ ├── Pods-Swiftilities_Example-acknowledgements.markdown │ │ │ └── Pods-Swiftilities_Example-acknowledgements.plist │ │ └── Swiftilities │ │ │ ├── Swiftilities.modulemap │ │ │ ├── Swiftilities-dummy.m │ │ │ ├── Swiftilities-prefix.pch │ │ │ ├── Swiftilities-umbrella.h │ │ │ ├── Swiftilities.xcconfig │ │ │ ├── Info.plist │ │ │ └── Swiftilities-Info.plist │ ├── Pods.xcodeproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Swiftilities.xcscheme │ └── Manifest.lock ├── Podfile ├── Swiftilities.xcodeproj │ └── project.xcworkspace │ │ └── contents.xcworkspacedata ├── Swiftilities.xcworkspace │ ├── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ └── contents.xcworkspacedata ├── fastlane │ ├── Fastfile │ └── README.md ├── Tests │ ├── Info.plist │ ├── LogTests.swift │ ├── RootViewControllerTests.swift │ ├── LicenseFormatterTests.swift │ ├── MathTests.swift │ └── TableViewHelperTests.swift └── Podfile.lock ├── .swiftlint.yml ├── Package.swift ├── .gitignore ├── LICENSE ├── .circleci └── config.yml ├── README.md └── CONTRIBUTING.md /Pod/Assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Pod/Classes/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /_Pods.xcodeproj: -------------------------------------------------------------------------------- 1 | Example/Pods/Pods.xcodeproj -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem 'cocoapods' 4 | gem 'xcpretty' 5 | gem 'fastlane' 6 | -------------------------------------------------------------------------------- /Example/Swiftilities/Assets/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Pod/Classes/HairlineView/HairlineTableCells.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rightpoint/Swiftilities/HEAD/Pod/Classes/HairlineView/HairlineTableCells.png -------------------------------------------------------------------------------- /Pod/Classes/TintedButton/simpleTintedButton.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rightpoint/Swiftilities/HEAD/Pod/Classes/TintedButton/simpleTintedButton.png -------------------------------------------------------------------------------- /Pod/Classes/Views/Gradient/GradientCustom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rightpoint/Swiftilities/HEAD/Pod/Classes/Views/Gradient/GradientCustom.png -------------------------------------------------------------------------------- /Pod/Classes/Views/Gradient/GradientSimple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rightpoint/Swiftilities/HEAD/Pod/Classes/Views/Gradient/GradientSimple.png -------------------------------------------------------------------------------- /Pod/Classes/Views/Textview/TailorTextView.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rightpoint/Swiftilities/HEAD/Pod/Classes/Views/Textview/TailorTextView.png -------------------------------------------------------------------------------- /Pod/Classes/HairlineView/HairlineButtonDivide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rightpoint/Swiftilities/HEAD/Pod/Classes/HairlineView/HairlineButtonDivide.png -------------------------------------------------------------------------------- /Pod/Classes/TintedButton/advancedTintedButton.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rightpoint/Swiftilities/HEAD/Pod/Classes/TintedButton/advancedTintedButton.png -------------------------------------------------------------------------------- /Pod/Classes/Views/Textview/PlaceholderTextSimple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rightpoint/Swiftilities/HEAD/Pod/Classes/Views/Textview/PlaceholderTextSimple.png -------------------------------------------------------------------------------- /Pod/Classes/Acknowledgements/AcknowledgementViewController.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rightpoint/Swiftilities/HEAD/Pod/Classes/Acknowledgements/AcknowledgementViewController.png -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-Swiftilities_Tests/Pods-Swiftilities_Tests-frameworks-Debug-output-files.xcfilelist: -------------------------------------------------------------------------------- 1 | ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Swiftilities.framework -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-Swiftilities_Tests/Pods-Swiftilities_Tests-frameworks-Release-output-files.xcfilelist: -------------------------------------------------------------------------------- 1 | ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Swiftilities.framework -------------------------------------------------------------------------------- /Pod/Classes/Acknowledgements/AcknowledgementsListViewController.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rightpoint/Swiftilities/HEAD/Pod/Classes/Acknowledgements/AcknowledgementsListViewController.png -------------------------------------------------------------------------------- /Pod/Classes/Lifecycle/Behaviors/Nav-Bar-Hairline-Fade/example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rightpoint/Swiftilities/HEAD/Pod/Classes/Lifecycle/Behaviors/Nav-Bar-Hairline-Fade/example.gif -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-Swiftilities_Example/Pods-Swiftilities_Example-frameworks-Debug-output-files.xcfilelist: -------------------------------------------------------------------------------- 1 | ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Swiftilities.framework -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-Swiftilities_Example/Pods-Swiftilities_Example-frameworks-Release-output-files.xcfilelist: -------------------------------------------------------------------------------- 1 | ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Swiftilities.framework -------------------------------------------------------------------------------- /Pod/Classes/Acknowledgements/CustomAcknowledgementViewController.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rightpoint/Swiftilities/HEAD/Pod/Classes/Acknowledgements/CustomAcknowledgementViewController.png -------------------------------------------------------------------------------- /Pod/Classes/Lifecycle/Behaviors/Nav-Bar-Title-Transition/example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rightpoint/Swiftilities/HEAD/Pod/Classes/Lifecycle/Behaviors/Nav-Bar-Title-Transition/example.gif -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Swiftilities/Swiftilities.modulemap: -------------------------------------------------------------------------------- 1 | framework module Swiftilities { 2 | umbrella header "Swiftilities-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /Pod/Classes/Acknowledgements/CustomAcknowledgementsListViewController.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rightpoint/Swiftilities/HEAD/Pod/Classes/Acknowledgements/CustomAcknowledgementsListViewController.png -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Swiftilities/Swiftilities-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_Swiftilities : NSObject 3 | @end 4 | @implementation PodsDummy_Swiftilities 5 | @end 6 | -------------------------------------------------------------------------------- /Example/Swiftilities/Assets/Images.xcassets/icn-twitter.imageset/498-twitter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rightpoint/Swiftilities/HEAD/Example/Swiftilities/Assets/Images.xcassets/icn-twitter.imageset/498-twitter.png -------------------------------------------------------------------------------- /Example/Swiftilities/Assets/Images.xcassets/icn-twitter.imageset/498-twitter@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rightpoint/Swiftilities/HEAD/Example/Swiftilities/Assets/Images.xcassets/icn-twitter.imageset/498-twitter@2x.png -------------------------------------------------------------------------------- /Example/Swiftilities/Assets/Images.xcassets/icn-twitter.imageset/498-twitter@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rightpoint/Swiftilities/HEAD/Example/Swiftilities/Assets/Images.xcassets/icn-twitter.imageset/498-twitter@3x.png -------------------------------------------------------------------------------- /Example/Pods/Pods.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/Swiftilities/Assets/Images.xcassets/logo-built-by-RZ.imageset/logo-built-by-RZ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rightpoint/Swiftilities/HEAD/Example/Swiftilities/Assets/Images.xcassets/logo-built-by-RZ.imageset/logo-built-by-RZ.png -------------------------------------------------------------------------------- /Example/Swiftilities/Assets/Images.xcassets/logo-built-by-RZ.imageset/logo-built-by-RZ@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rightpoint/Swiftilities/HEAD/Example/Swiftilities/Assets/Images.xcassets/logo-built-by-RZ.imageset/logo-built-by-RZ@2x.png -------------------------------------------------------------------------------- /Example/Swiftilities/Assets/Images.xcassets/logo-built-by-RZ.imageset/logo-built-by-RZ@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rightpoint/Swiftilities/HEAD/Example/Swiftilities/Assets/Images.xcassets/logo-built-by-RZ.imageset/logo-built-by-RZ@3x.png -------------------------------------------------------------------------------- /Example/Podfile: -------------------------------------------------------------------------------- 1 | platform :ios, '9.0' 2 | use_frameworks! 3 | 4 | target 'Swiftilities_Example' do 5 | pod 'Swiftilities', :path => '../' 6 | end 7 | 8 | target 'Swiftilities_Tests' do 9 | pod 'Swiftilities', :path => '../' 10 | end 11 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-Swiftilities_Tests/Pods-Swiftilities_Tests.modulemap: -------------------------------------------------------------------------------- 1 | framework module Pods_Swiftilities_Tests { 2 | umbrella header "Pods-Swiftilities_Tests-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-Swiftilities_Example/Pods-Swiftilities_Example.modulemap: -------------------------------------------------------------------------------- 1 | framework module Pods_Swiftilities_Example { 2 | umbrella header "Pods-Swiftilities_Example-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-Swiftilities_Tests/Pods-Swiftilities_Tests-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_Pods_Swiftilities_Tests : NSObject 3 | @end 4 | @implementation PodsDummy_Pods_Swiftilities_Tests 5 | @end 6 | -------------------------------------------------------------------------------- /Example/Swiftilities.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-Swiftilities_Example/Pods-Swiftilities_Example-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_Pods_Swiftilities_Example : NSObject 3 | @end 4 | @implementation PodsDummy_Pods_Swiftilities_Example 5 | @end 6 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-Swiftilities_Tests/Pods-Swiftilities_Tests-frameworks-Debug-input-files.xcfilelist: -------------------------------------------------------------------------------- 1 | ${PODS_ROOT}/Target Support Files/Pods-Swiftilities_Tests/Pods-Swiftilities_Tests-frameworks.sh 2 | ${BUILT_PRODUCTS_DIR}/Swiftilities/Swiftilities.framework -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-Swiftilities_Tests/Pods-Swiftilities_Tests-frameworks-Release-input-files.xcfilelist: -------------------------------------------------------------------------------- 1 | ${PODS_ROOT}/Target Support Files/Pods-Swiftilities_Tests/Pods-Swiftilities_Tests-frameworks.sh 2 | ${BUILT_PRODUCTS_DIR}/Swiftilities/Swiftilities.framework -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-Swiftilities_Example/Pods-Swiftilities_Example-frameworks-Debug-input-files.xcfilelist: -------------------------------------------------------------------------------- 1 | ${PODS_ROOT}/Target Support Files/Pods-Swiftilities_Example/Pods-Swiftilities_Example-frameworks.sh 2 | ${BUILT_PRODUCTS_DIR}/Swiftilities/Swiftilities.framework -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-Swiftilities_Example/Pods-Swiftilities_Example-frameworks-Release-input-files.xcfilelist: -------------------------------------------------------------------------------- 1 | ${PODS_ROOT}/Target Support Files/Pods-Swiftilities_Example/Pods-Swiftilities_Example-frameworks.sh 2 | ${BUILT_PRODUCTS_DIR}/Swiftilities/Swiftilities.framework -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Swiftilities/Swiftilities-prefix.pch: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | -------------------------------------------------------------------------------- /Example/Swiftilities.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/Pods/Pods.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Swiftilities/Swiftilities-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | 14 | FOUNDATION_EXPORT double SwiftilitiesVersionNumber; 15 | FOUNDATION_EXPORT const unsigned char SwiftilitiesVersionString[]; 16 | 17 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-Swiftilities_Tests/Pods-Swiftilities_Tests-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | 14 | FOUNDATION_EXPORT double Pods_Swiftilities_TestsVersionNumber; 15 | FOUNDATION_EXPORT const unsigned char Pods_Swiftilities_TestsVersionString[]; 16 | 17 | -------------------------------------------------------------------------------- /Pod/Classes/Compatibility/Compatibility.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Compatibility.swift 3 | // Swiftilities 4 | // 5 | // Created by Brian King on 8/24/16. 6 | // Copyright © 2016 Raizlabs. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | #if swift(>=4.1) 12 | #else 13 | extension Collection { 14 | func compactMap(_ transform: (Element) throws -> ElementOfResult?) rethrows -> [ElementOfResult] { 15 | return try flatMap(transform) 16 | } 17 | } 18 | #endif 19 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-Swiftilities_Example/Pods-Swiftilities_Example-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | 14 | FOUNDATION_EXPORT double Pods_Swiftilities_ExampleVersionNumber; 15 | FOUNDATION_EXPORT const unsigned char Pods_Swiftilities_ExampleVersionString[]; 16 | 17 | -------------------------------------------------------------------------------- /Example/Swiftilities/Assets/Images.xcassets/logo-built-by-RZ.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "logo-built-by-RZ.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "logo-built-by-RZ@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "logo-built-by-RZ@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /Example/Swiftilities/Views/Lifecycle/LogAppearanceBehavior.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LogAppearanceBehavior.swift 3 | // Swiftilities 4 | // 5 | // Created by Michael Skiba on 2/23/17. 6 | // Copyright © 2017 Raizlabs. All rights reserved. 7 | // 8 | 9 | import Swiftilities 10 | import UIKit 11 | 12 | struct LogAppearanceBehavior: ViewControllerLifecycleBehavior { 13 | 14 | public func beforeAppearing(_ viewController: UIViewController, animated: Bool) { 15 | // Logger 16 | 17 | Log.logLevel = .info 18 | Log.info("\(type(of: viewController))") 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /Pod/Classes/TableViewHelpers/UITableView+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UITableView+Extensions.swift 3 | // Swiftilities 4 | // 5 | // Created by Zev Eisenberg on 4/19/17. 6 | // Copyright © 2017 Raizlabs. All rights reserved. 7 | // 8 | 9 | #if canImport(UIKit) 10 | import UIKit 11 | 12 | public extension UITableView { 13 | 14 | func role(ofRow indexPath: IndexPath) -> IndexPath.RowRole { 15 | let rowsInSection = numberOfRows(inSection: indexPath.section) 16 | 17 | return indexPath.role(inSectionWithNumberOfRows: rowsInSection) 18 | } 19 | 20 | } 21 | 22 | #endif 23 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | - cyclomatic_complexity 3 | - file_length 4 | - function_body_length 5 | - function_parameter_count 6 | - line_length 7 | - nesting 8 | - todo 9 | - type_body_length 10 | - conditional_binding_cascade 11 | 12 | opt_in_rules: 13 | - vertical_whitespace 14 | - operator_usage_whitespace 15 | - class_delegate_protocol 16 | - sorted_imports 17 | 18 | trailing_comma: 19 | mandatory_comma: true 20 | 21 | statement_position: 22 | statement_mode: uncuddled_else 23 | 24 | excluded: 25 | - Pods 26 | 27 | swiftlint_version: 28 | - 0.18.1 29 | -------------------------------------------------------------------------------- /Example/fastlane/Fastfile: -------------------------------------------------------------------------------- 1 | # This file contains the fastlane.tools configuration 2 | # You can find the documentation at https://docs.fastlane.tools 3 | # 4 | # For a list of all available actions, check out 5 | # 6 | # https://docs.fastlane.tools/actions 7 | # 8 | # For a list of all available plugins, check out 9 | # 10 | # https://docs.fastlane.tools/plugins/available-plugins 11 | # 12 | 13 | default_platform(:ios) 14 | 15 | platform :ios do 16 | desc "Runs tests on iOS" 17 | lane :test do 18 | scan( 19 | output_types: 'junit', 20 | scheme: 'Swiftilities-Example', 21 | ) 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /Example/Swiftilities/Assets/Images.xcassets/icn-twitter.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "498-twitter.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "498-twitter@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "498-twitter@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | }, 23 | "properties" : { 24 | "template-rendering-intent" : "template" 25 | } 26 | } -------------------------------------------------------------------------------- /Example/Swiftilities.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 10 | 11 | 14 | 15 | 17 | 18 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /Pod/Classes/Lifecycle/Behaviors/Nav-Bar-Hairline-Fade/readme.md: -------------------------------------------------------------------------------- 1 | # NavBarHarilineFadeBehavior 2 | 3 | This behavior adds a hairline to the bottom of a `UINavigationBar` and animates its `alpha` alongside a `UIScrollView`'s `contentOffset` 4 | 5 | ![](example.gif) 6 | 7 | ## Usage 8 | ```swift 9 | override func viewDidLoad() { 10 | super.viewDidLoad() 11 | 12 | let behavior = NavBarHarilineFadeBehavior(scrollView: scrollView) 13 | 14 | //optional configuration 15 | behavior.contentOffsetFadeRange = 0...100 16 | behavior.hairlineColor = .lightGray 17 | behavior.hairlineThickenss = 1 18 | 19 | addBehaviors([behavior]) 20 | } 21 | ``` 22 | -------------------------------------------------------------------------------- /Example/Swiftilities/App Delegate/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Swiftilities 4 | // 5 | // Created by Nicholas Bonatsakis on 02/05/2016. 6 | // Copyright (c) 2016 Nicholas Bonatsakis. All rights reserved. 7 | // 8 | 9 | import Swiftilities 10 | import UIKit 11 | 12 | @UIApplicationMain 13 | class AppDelegate: UIResponder, UIApplicationDelegate { 14 | var window: UIWindow? 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { 17 | DefaultBehaviors(behaviors: [LogAppearanceBehavior()]).inject() 18 | return true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Swiftilities/Swiftilities.xcconfig: -------------------------------------------------------------------------------- 1 | CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/Swiftilities 2 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 3 | OTHER_LDFLAGS = $(inherited) -framework "CoreData" -framework "CoreGraphics" -framework "Foundation" -framework "MessageUI" -framework "UIKit" 4 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS 5 | PODS_BUILD_DIR = ${BUILD_DIR} 6 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 7 | PODS_ROOT = ${SRCROOT} 8 | PODS_TARGET_SRCROOT = ${PODS_ROOT}/../.. 9 | PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} 10 | SKIP_INSTALL = YES 11 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 12 | -------------------------------------------------------------------------------- /Example/fastlane/README.md: -------------------------------------------------------------------------------- 1 | fastlane documentation 2 | ================ 3 | # Installation 4 | 5 | Make sure you have the latest version of the Xcode command line tools installed: 6 | 7 | ``` 8 | xcode-select --install 9 | ``` 10 | 11 | Install _fastlane_ using 12 | ``` 13 | [sudo] gem install fastlane -NV 14 | ``` 15 | or alternatively using `brew cask install fastlane` 16 | 17 | # Available Actions 18 | ## iOS 19 | ### ios test 20 | ``` 21 | fastlane ios test 22 | ``` 23 | Runs tests on iOS 24 | 25 | ---- 26 | 27 | This README.md is auto-generated and will be re-generated every time [fastlane](https://fastlane.tools) is run. 28 | More information about fastlane can be found on [fastlane.tools](https://fastlane.tools). 29 | The documentation of fastlane can be found on [docs.fastlane.tools](https://docs.fastlane.tools). 30 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Swiftilities", 8 | platforms: [ 9 | .iOS(.v9), 10 | .macOS(.v10_11), 11 | .tvOS(.v9), 12 | .watchOS(.v2), 13 | ], 14 | products: [ 15 | .library( 16 | name: "Swiftilities", 17 | targets: ["Swiftilities"]), 18 | ], 19 | targets: [ 20 | .target( 21 | name: "Swiftilities", 22 | dependencies: [], 23 | path: "Pod/Classes"), 24 | .testTarget( 25 | name: "SwiftilitiesTests", 26 | dependencies: ["Swiftilities"], 27 | path: "Example/Tests"), 28 | ] 29 | ) 30 | -------------------------------------------------------------------------------- /Example/Tests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS X 2 | .DS_Store 3 | 4 | # Xcode 5 | build/ 6 | *.pbxuser 7 | !default.pbxuser 8 | *.mode1v3 9 | !default.mode1v3 10 | *.mode2v3 11 | !default.mode2v3 12 | *.perspectivev3 13 | !default.perspectivev3 14 | xcuserdata 15 | *.xccheckout 16 | profile 17 | *.moved-aside 18 | DerivedData 19 | *.hmap 20 | *.ipa 21 | 22 | # Bundler 23 | .bundle 24 | 25 | Carthage 26 | # We recommend against adding the Pods directory to your .gitignore. However 27 | # you should judge for yourself, the pros and cons are mentioned at: 28 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control 29 | # 30 | # Note: if you ignore the Pods directory, make sure to uncomment 31 | # `pod install` in .travis.yml 32 | # 33 | # Pods/ 34 | 35 | Example/fastlane/report.xml 36 | Example/fastlane/test_output 37 | 38 | # Swift Package Manager 39 | .swiftpm/ 40 | .build/ 41 | -------------------------------------------------------------------------------- /Pod/Classes/Lifecycle/Behaviors/Nav-Bar-Title-Transition/readme.md: -------------------------------------------------------------------------------- 1 | # NavTitleTransitionBehavior.swift 2 | 3 | This behavior manages the interactivity between a title, anywhere in a `UIScrollView` and the title of a `UINavigationBar`. The behavior is defined so as the title subview scrolls under the title, the Nav Bar's title will translate into place. 4 | 5 | ![](example.gif) 6 | 7 | This behavior is accomplished by leveraging the `UINavigationBar().titleVerticalPositionAdjustment`, allowing the title to play nicely with other `UINavigationItem`s, (since it is using the normal `navigationItem.title`.) 8 | 9 | ## Usage 10 | ```swift 11 | override func viewDidLoad() { 12 | super.viewDidLoad() 13 | navigationItem.setTitle("Title Transition") 14 | let behavior = NavTitleTransitionBehavior(scrollView: scrollView, titleView: titleLabel) 15 | addBehaviors([behavior]) 16 | } 17 | ``` 18 | -------------------------------------------------------------------------------- /Pod/Classes/Math/Clamping.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Clamping.swift 3 | // Swiftilities 4 | // 5 | // Created by John Stricker on 5/3/16. 6 | // Copyright © 2016 Raizlabs. All rights reserved. 7 | // 8 | 9 | public extension Comparable { 10 | 11 | /// Clamp a value to a `ClosedRange`. 12 | /// 13 | /// - Parameter to: a `ClosedRange` whose start and end specify the clamp's minimum and maximum. 14 | /// - Returns: the clamped value. 15 | func clamped(to range: ClosedRange) -> Self { 16 | return clamped(min: range.lowerBound, max: range.upperBound) 17 | } 18 | 19 | /// Clamp a value to a minimum and maximum value. 20 | /// 21 | /// - Parameters: 22 | /// - lower: the minimum value allowed. 23 | /// - upper: the maximum value allowed. 24 | /// - Returns: the clamped value. 25 | func clamped(min lower: Self, max upper: Self) -> Self { 26 | return min(max(self, lower), upper) 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-Swiftilities_Tests/Pods-Swiftilities_Tests.debug.xcconfig: -------------------------------------------------------------------------------- 1 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES 2 | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Swiftilities" 3 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 4 | HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Swiftilities/Swiftilities.framework/Headers" 5 | LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' 6 | OTHER_LDFLAGS = $(inherited) -framework "CoreData" -framework "CoreGraphics" -framework "Foundation" -framework "MessageUI" -framework "Swiftilities" -framework "UIKit" 7 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS 8 | PODS_BUILD_DIR = ${BUILD_DIR} 9 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 10 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/. 11 | PODS_ROOT = ${SRCROOT}/Pods 12 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 13 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-Swiftilities_Example/Pods-Swiftilities_Example.debug.xcconfig: -------------------------------------------------------------------------------- 1 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES 2 | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Swiftilities" 3 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 4 | HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Swiftilities/Swiftilities.framework/Headers" 5 | LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' 6 | OTHER_LDFLAGS = $(inherited) -framework "CoreData" -framework "CoreGraphics" -framework "Foundation" -framework "MessageUI" -framework "Swiftilities" -framework "UIKit" 7 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS 8 | PODS_BUILD_DIR = ${BUILD_DIR} 9 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 10 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/. 11 | PODS_ROOT = ${SRCROOT}/Pods 12 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 13 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-Swiftilities_Tests/Pods-Swiftilities_Tests.release.xcconfig: -------------------------------------------------------------------------------- 1 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES 2 | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Swiftilities" 3 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 4 | HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Swiftilities/Swiftilities.framework/Headers" 5 | LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' 6 | OTHER_LDFLAGS = $(inherited) -framework "CoreData" -framework "CoreGraphics" -framework "Foundation" -framework "MessageUI" -framework "Swiftilities" -framework "UIKit" 7 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS 8 | PODS_BUILD_DIR = ${BUILD_DIR} 9 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 10 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/. 11 | PODS_ROOT = ${SRCROOT}/Pods 12 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 13 | -------------------------------------------------------------------------------- /Example/Swiftilities/Views/Text Formatting/TextFormattingViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TextFormattingViewController.swift 3 | // Swiftilities 4 | // 5 | // Created by Nicholas Bonatsakis on 02/05/2016. 6 | // Copyright (c) 2016 Nicholas Bonatsakis. All rights reserved. 7 | // 8 | 9 | import Swiftilities 10 | import UIKit 11 | 12 | class TextFormattingViewController: UIViewController { 13 | 14 | @IBOutlet var allCapsTextField: FormattedTextField! 15 | @IBOutlet var onlyNumbersTextField: FormattedTextField! 16 | 17 | override func viewDidLoad() { 18 | super.viewDidLoad() 19 | 20 | allCapsTextField.formatter = { string in 21 | return string?.uppercased() 22 | } 23 | 24 | let nonNumeric = NSCharacterSet.decimalDigits.inverted 25 | 26 | onlyNumbersTextField.formatter = { string in 27 | return string?.components(separatedBy: nonNumeric).joined(separator: "") 28 | } 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-Swiftilities_Example/Pods-Swiftilities_Example.release.xcconfig: -------------------------------------------------------------------------------- 1 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES 2 | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Swiftilities" 3 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 4 | HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Swiftilities/Swiftilities.framework/Headers" 5 | LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' 6 | OTHER_LDFLAGS = $(inherited) -framework "CoreData" -framework "CoreGraphics" -framework "Foundation" -framework "MessageUI" -framework "Swiftilities" -framework "UIKit" 7 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS 8 | PODS_BUILD_DIR = ${BUILD_DIR} 9 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 10 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/. 11 | PODS_ROOT = ${SRCROOT}/Pods 12 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 13 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Swiftilities/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 0.19.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-Swiftilities_Tests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-Swiftilities_Example/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Swiftilities/Swiftilities-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 0.25.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Example/Tests/LogTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LogTests.swift 3 | // Swiftilities 4 | // 5 | // Created by Zev Eisenberg on 4/13/17. 6 | // Copyright © 2017 Raizlabs. All rights reserved. 7 | // 8 | 9 | import Swiftilities 10 | import XCTest 11 | 12 | class LogTests: XCTestCase { 13 | 14 | func testLogHandler() { 15 | 16 | var loggedStrings: [(level: Log.Level, string: String)] = [] 17 | Log.handler = { (level, string) in 18 | loggedStrings.append((level: level, string: string)) 19 | } 20 | 21 | Log.logLevel = .warn 22 | 23 | Log.verbose("Ignore me") 24 | Log.error("Don't ignore me!") 25 | 26 | // Log messages include the date, which is not conducive to testing, 27 | // so we just check the end of the logged string. 28 | XCTAssertEqual(loggedStrings.count, 1) 29 | XCTAssertEqual(loggedStrings[0].level, .error) 30 | XCTAssertTrue(loggedStrings[0].string.hasSuffix("Don't ignore me!")) 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /Pod/Classes/TableViewHelpers/TableSection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableSection.swift 3 | // Swiftilities 4 | // 5 | // Created by Zev Eisenberg on 4/19/17. 6 | // Copyright © 2017 Raizlabs. All rights reserved. 7 | // 8 | 9 | #if canImport(UIKit) 10 | import UIKit 11 | 12 | public struct TableSection: RowContainer { 13 | 14 | public typealias Row = RowType 15 | 16 | public var section: SectionType 17 | public var rows: [RowType] 18 | 19 | public init(section: SectionType, rows: [RowType]) { 20 | self.section = section 21 | self.rows = rows 22 | } 23 | 24 | } 25 | 26 | public protocol RowContainer { 27 | 28 | associatedtype Row 29 | 30 | var rows: [Row] { get set } 31 | 32 | } 33 | 34 | public extension Collection where Element: RowContainer, Self.Index == Int { 35 | 36 | subscript(indexPath indexPath: IndexPath) -> Iterator.Element.Row { 37 | return self[indexPath.section].rows[indexPath.row] 38 | } 39 | 40 | } 41 | 42 | #endif 43 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-Swiftilities_Tests/Pods-Swiftilities_Tests-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-Swiftilities_Example/Pods-Swiftilities_Example-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Example/Swiftilities/Assets/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | } 43 | ], 44 | "info" : { 45 | "version" : 1, 46 | "author" : "xcode" 47 | } 48 | } -------------------------------------------------------------------------------- /Example/Swiftilities/Views/Keyboard Avoidance/KeyboardAvoidanceViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyboardAvoidanceViewController.swift 3 | // Swiftilities 4 | // 5 | // Created by Michael Skiba on 11/11/16. 6 | // Copyright © 2016 CocoaPods. All rights reserved. 7 | // 8 | 9 | import Swiftilities 10 | import UIKit 11 | 12 | class KeyboardAvoidanceViewController: UIViewController { 13 | 14 | @IBOutlet weak var textField: UITextField! 15 | 16 | override func viewDidLoad() { 17 | super.viewDidLoad() 18 | textField.delegate = self 19 | 20 | view.addKeyboardLayoutGuide().topAnchor.constraint(greaterThanOrEqualTo: textField.bottomAnchor, constant: 10).isActive = true 21 | } 22 | 23 | override func viewWillAppear(_ animated: Bool) { 24 | super.viewWillAppear(animated) 25 | textField.becomeFirstResponder() 26 | } 27 | 28 | } 29 | 30 | extension KeyboardAvoidanceViewController: UITextFieldDelegate { 31 | 32 | func textFieldShouldReturn(_ textField: UITextField) -> Bool { 33 | textField.resignFirstResponder() 34 | return true 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /Pod/Classes/StackViewHelpers/UIStackView+Helpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIStackView+Helpers.swift 3 | // Swiftilities 4 | // 5 | // Created by Michael Skiba on 11/17/16. 6 | // Copyright © 2016 Raizlabs. All rights reserved. 7 | 8 | #if canImport(UIKit) 9 | import UIKit 10 | 11 | public extension UIStackView { 12 | 13 | convenience init(axis: NSLayoutConstraint.Axis, arrangedSubviews: UIView...) { 14 | self.init(axis: axis, arrangedSubviews: arrangedSubviews) 15 | } 16 | 17 | convenience init(axis: NSLayoutConstraint.Axis, arrangedSubviews: [UIView]) { 18 | self.init(arrangedSubviews: arrangedSubviews) 19 | self.axis = axis 20 | } 21 | 22 | func addArrangedSubviews(_ views: UIView...) { 23 | addArrangedSubviews(views) 24 | } 25 | 26 | func addArrangedSubviews(_ views: [UIView]) { 27 | views.forEach { 28 | addArrangedSubview($0) 29 | } 30 | } 31 | 32 | func removeAllArrangedSubviews() { 33 | arrangedSubviews.forEach { 34 | $0.removeFromSuperview() 35 | } 36 | } 37 | 38 | 39 | } 40 | 41 | #endif 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2016 Raizlabs and other contributors 2 | http://raizlabs.com/ 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining 5 | a copy of this software and associated documentation files (the 6 | "Software"), to deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, merge, publish, 8 | distribute, sublicense, and/or sell copies of the Software, and to 9 | permit persons to whom the Software is furnished to do so, subject to 10 | the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Example/Swiftilities/Views/About View/AboutViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AboutViewController.swift 3 | // Swiftilities 4 | // 5 | // Created by Michael Skiba on 2/28/17. 6 | // Copyright © 2017 Raizlabs. All rights reserved. 7 | // 8 | 9 | import Swiftilities 10 | import UIKit 11 | 12 | class AboutViewController: UIViewController, FeedbackPresenter { 13 | 14 | @IBOutlet weak var actionStack: UIStackView! 15 | 16 | override func viewDidLoad() { 17 | super.viewDidLoad() 18 | let aboutView = AboutView(image: #imageLiteral(resourceName: "logo-built-by-RZ"), imageAccessibilityLabel: "Designed and developed by Raizlabs") 19 | actionStack.addArrangedSubview(aboutView) 20 | } 21 | 22 | @IBAction func showMailComposer(_ sender: UIView) { 23 | presentSendFeedback(to: "feedback@raizlabs.com") { _ in 24 | Log.info("Email done") 25 | } 26 | } 27 | 28 | @IBAction func showShareApp(_ sender: UIView) { 29 | guard let url = URL(string: "http://raizlabs.com") else { 30 | return 31 | } 32 | presentShareApp(shareText: "Share App", shareURL: url, presentedFrom: sender) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Pod/Classes/Lifecycle/Framework/UIViewController+Lifecycle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewController+Lifecycle.swift 3 | // Swiftilities 4 | // 5 | // Created by Nicholas Bonatsakis on 6/10/16. 6 | // Copyright© 2016 Raizlabs 7 | // 8 | // Based on concepts and examples in: 9 | // http://khanlou.com/2016/02/many-controllers/ and 10 | // http://irace.me/lifecycle-behaviors 11 | 12 | #if canImport(UIKit) 13 | import UIKit 14 | 15 | public extension UIViewController { 16 | 17 | /// Add behaviors to be hooked into this view controller’s lifecycle. 18 | /// 19 | /// This method requires the view controller’s view to be loaded, so it’s best to call 20 | /// in `viewDidLoad` to avoid it being loaded prematurely. 21 | /// 22 | /// - Parameter behaviors: The behaviors to add 23 | func addBehaviors(_ behaviors: [ViewControllerLifecycleBehavior]) { 24 | let behaviorViewController = LifecycleBehaviorViewController(behaviors: behaviors) 25 | 26 | loadViewIfNeeded() 27 | addChild(behaviorViewController) 28 | view.addSubview(behaviorViewController.view) 29 | behaviorViewController.didMove(toParent: self) 30 | } 31 | 32 | } 33 | 34 | #endif 35 | -------------------------------------------------------------------------------- /Pod/Classes/Lifecycle/readme.md: -------------------------------------------------------------------------------- 1 | # View Controller Lifecycle Behaviors 2 | 3 | This is a collection of handy [View Controller Lifecycle Behaviors](http://irace.me/lifecycle-behaviors), useful for formalizing reusable bits of common lifecycle-dependent functionality. 4 | 5 | ## Included Behaviors 6 | 7 | - [Nav Bar Hairline Fade](Behaviors/Nav-Bar-Hairline-Fade) 8 | - [Nav Bar Title Transition](Behaviors/Nav-Bar-Title-Transition) 9 | 10 | 11 | # Usage 12 | 13 | Behaviors are implemented by consuming `ViewController`s simply by leveraging the category method `addBehaviors(_ behaviors: [ViewControllerLifecycleBehavior]` in `viewDidLoad`: 14 | ``` 15 | override func viewDidLoad() { 16 | super.viewDidLoad() 17 | let behavior1 = MyBehavior() 18 | let behavior2 = AnotherBehavior() 19 | addBehaviors([behavior1, behavior2]) 20 | } 21 | ``` 22 | 23 | ## Default Behaviors 24 | 25 | Default behaviors are behaviors that are automatically injected into all view controllers using method swizzling. This may be convenient for functionality akin to analytics screen tagging, for instance. Default behaviors are injected using: 26 | ``` 27 | let behavior = MyBehavior() 28 | DefaultBehaviors(behaviors: [behavior]).inject() 29 | ``` 30 | -------------------------------------------------------------------------------- /Pod/Classes/DeviceSize/CGSize+DeviceSize.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGSize+DeviceSize.swift 3 | // Swiftilities 4 | // 5 | // Created by Rob Cadwallader on 11/4/16. 6 | // 7 | // 8 | 9 | #if canImport(UIKit) 10 | import UIKit 11 | 12 | public extension CGSize { 13 | 14 | /** 15 | Calculates the ratio of a CGSize to a reference DeviceSize and then returns that ratio 16 | multiplied by the current DeviceSize. For example: 17 | 18 | let value: CGSize = CGSize(width: 100, height: 100) 19 | let result = value.proportional(toDeviceSize: .small) 20 | print(result) // Prints (117.1875, 138.958333333333) when DeviceSize.current == .large 21 | 22 | - parameter size: Reference DeviceSize value 23 | */ 24 | func proportional(toDeviceSize size: DeviceSize) -> CGSize { 25 | let xDimension = size.dimensions.width 26 | let yDimension = size.dimensions.height 27 | guard xDimension != 0 && yDimension != 0 else { 28 | return self 29 | } 30 | 31 | let xRatio: CGFloat = self.width / xDimension 32 | let yRatio: CGFloat = self.height / yDimension 33 | 34 | return CGSize(width: (DeviceSize.current.dimensions.width * xRatio), height: (DeviceSize.current.dimensions.height * yRatio)) 35 | } 36 | 37 | } 38 | 39 | #endif 40 | -------------------------------------------------------------------------------- /Example/Swiftilities/Views/Tinted Buttons/TintedButtonsViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TintedButtonsViewController.swift 3 | // Swiftilities 4 | // 5 | // Created by Michael Skiba on 11/17/16. 6 | // Copyright © 2016 Raizlabs. All rights reserved. 7 | // 8 | 9 | import Swiftilities 10 | import UIKit 11 | 12 | class TintedButtonsViewController: UIViewController { 13 | 14 | @IBOutlet weak var buttonStack: UIStackView! 15 | var mutatedButton = TintedButton(fillColor: .red, textColor: .red) 16 | 17 | override func viewDidLoad() { 18 | super.viewDidLoad() 19 | 20 | let black = UIColor(hex: 0x000000) 21 | let lightBlue = UIColor(rgba: 0x5555FFFF) 22 | let newButton = TintedButton(fillColor: black, textColor: lightBlue) 23 | newButton.setTitle("Button 5", for: .normal) 24 | mutatedButton.setTitle("Button 6", for: .normal) 25 | buttonStack.addArrangedSubview(newButton) 26 | buttonStack.addArrangedSubview(mutatedButton) 27 | } 28 | 29 | override func viewWillAppear(_ animated: Bool) { 30 | super.viewWillAppear(animated) 31 | mutatedButton.buttonBorderWidth = 3 32 | mutatedButton.buttonCornerRadius = 6 33 | mutatedButton.fillColor = .lightGray 34 | mutatedButton.textColor = .darkText 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /Example/Swiftilities/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 0.10.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /Pod/Classes/DeviceSize/CGFloat+DeviceSize.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGFloat+DeviceSize.swift 3 | // Swiftilities 4 | // 5 | // Created by Rob Cadwallader on 11/4/16. 6 | // 7 | // 8 | 9 | #if canImport(UIKit) 10 | import UIKit 11 | 12 | public extension CGFloat { 13 | 14 | /** 15 | Calculates the ratio of a CGFloat to an axis of a reference DeviceSize and then returns that ratio 16 | multiplied by the same axis of the current DeviceSize. For example: 17 | 18 | let value: CGFloat = 100.0 19 | let result = value.proportional(toDeviceSize: .small, axis: .y) 20 | print(result) // Prints 138.958333333333 when DeviceSize.current == .large 21 | 22 | - parameter size: Reference DeviceSize value 23 | - parameter axis: Axis to compare 24 | */ 25 | func proportional(toDeviceSize size: DeviceSize, axis: Axis) -> CGFloat { 26 | let defaultDimension = (axis == .x) ? size.dimensions.width : size.dimensions.height 27 | guard defaultDimension != 0, size != DeviceSize.current else { 28 | return self 29 | } 30 | 31 | let ratio: CGFloat = self / defaultDimension 32 | let currentDimension: CGFloat = (axis == .x) ? DeviceSize.current.dimensions.width : DeviceSize.current.dimensions.height 33 | 34 | return ratio * currentDimension 35 | } 36 | 37 | } 38 | 39 | #endif 40 | -------------------------------------------------------------------------------- /Pod/Classes/Logging/README.md: -------------------------------------------------------------------------------- 1 | # Logging 2 | 3 | Set a logging level to dictate priority of events logged 4 | 5 | ### Quick Start 6 | 7 | By default, nothing will be logged, so you want to set a logging level during app set-up, before any loggable events. Different logging levels are often set for different build schemes (debug scheme may be `.verbose` while release might be `.warn`). 8 | ```swift 9 | Log.logLevel = .warn 10 | ``` 11 | 12 | Use the level functions when logging to indicate what type of event has occurred 13 | ```swift 14 | Log.verbose("Lower priority events will not be logged") 15 | Log.error("Errors are higher priority than .warn, so will be logged") 16 | ``` 17 | 18 | Log levels (from highest to lowest priority): 19 | - verbose 20 | - debug 21 | - info 22 | - warn 23 | - error 24 | - off 25 | 26 | ## Advanced functionality 27 | 28 | You can include one custom handler that will get called for any string being logged. Assigning another handler will replace the first. 29 | 30 | ```swift 31 | Log.handler = { (level, string) in 32 | sendToAnalytics((key: level, string: string)) 33 | } 34 | ``` 35 | 36 | Log level can also be represented by emoji instead of strings. 37 | 38 | ```swift 39 | Log.useEmoji = true 40 | ``` 41 | 42 | Emoji key: 43 | - .verbose = 📖 44 | - .debug = 🐝 45 | - .info = ✏️ 46 | - .warn = ⚠️ 47 | - .error = ⁉️ 48 | -------------------------------------------------------------------------------- /Pod/Classes/TableViewHelpers/IndexPath+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IndexPath+Extensions.swift 3 | // Swiftilities 4 | // 5 | // Created by Zev Eisenberg on 4/19/17. 6 | // Copyright © 2017 Swiftilities. All rights reserved. 7 | // 8 | 9 | #if canImport(UIKit) 10 | import UIKit 11 | 12 | public extension IndexPath { 13 | 14 | /// The role that a table view cell plays in a table view section. 15 | enum RowRole { 16 | 17 | /// This cell is the only one in its section. 18 | case only 19 | 20 | /// This cell is the first row in its section. 21 | case first 22 | 23 | /// This cell is not the first first, last, or only row in its section. 24 | case middle 25 | 26 | /// This cell is the last row in its section. 27 | case last 28 | 29 | } 30 | 31 | } 32 | 33 | public extension IndexPath { 34 | 35 | func role(inSectionWithNumberOfRows rowsInSection: Int) -> RowRole { 36 | guard rowsInSection != 0 else { 37 | preconditionFailure("Attempt to assess role of index path in section with zero items") 38 | } 39 | 40 | guard rowsInSection > 1 else { 41 | return .only 42 | } 43 | 44 | switch row { 45 | case 0: return .first 46 | case rowsInSection - 1: return .last 47 | default: return .middle 48 | } 49 | } 50 | 51 | } 52 | 53 | #endif 54 | -------------------------------------------------------------------------------- /Example/Swiftilities/Views/GradientViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GradientViewController.swift 3 | // Swiftilities 4 | // 5 | // Created by Nicholas Bonatsakis on 5/1/17. 6 | // Copyright © 2017 Raizlabs. All rights reserved. 7 | // 8 | 9 | import Swiftilities 10 | import UIKit 11 | 12 | class GradientViewController: UIViewController { 13 | 14 | @IBOutlet weak var stackView: UIStackView! 15 | 16 | override func viewDidLoad() { 17 | super.viewDidLoad() 18 | 19 | let gradients = [ 20 | GradientView(direction: .leftToRight, colors: [.red, .white, .blue]), 21 | GradientView(direction: .topToBottom, colors: [.green, .black, .orange]), 22 | GradientView( 23 | direction: .custom(start: CGPoint(x: 0, y: 0), end: CGPoint(x: 1, y: 1)), 24 | colors: [.blue, .orange, .purple] 25 | ), 26 | GradientView(direction: .topToBottom, colors: [.purple, .black], locations: [0.9, 1.0]), 27 | ] 28 | 29 | gradients.forEach { view in 30 | stackView.addArrangedSubview(view) 31 | 32 | view.widthAnchor.constraint(equalTo: stackView.widthAnchor).isActive = true 33 | view.heightAnchor.constraint(equalToConstant: 64.0).isActive = true 34 | 35 | view.layer.masksToBounds = true 36 | view.layer.cornerRadius = 6.0 37 | } 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-Swiftilities_Tests/Pods-Swiftilities_Tests-acknowledgements.markdown: -------------------------------------------------------------------------------- 1 | # Acknowledgements 2 | This application makes use of the following third party libraries: 3 | 4 | ## Swiftilities 5 | 6 | Copyright 2016 Raizlabs and other contributors 7 | http://raizlabs.com/ 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining 10 | a copy of this software and associated documentation files (the 11 | "Software"), to deal in the Software without restriction, including 12 | without limitation the rights to use, copy, modify, merge, publish, 13 | distribute, sublicense, and/or sell copies of the Software, and to 14 | permit persons to whom the Software is furnished to do so, subject to 15 | the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be 18 | included in all copies or substantial portions of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 21 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 22 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 23 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 24 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 25 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 26 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 27 | Generated by CocoaPods - https://cocoapods.org 28 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-Swiftilities_Example/Pods-Swiftilities_Example-acknowledgements.markdown: -------------------------------------------------------------------------------- 1 | # Acknowledgements 2 | This application makes use of the following third party libraries: 3 | 4 | ## Swiftilities 5 | 6 | Copyright 2016 Raizlabs and other contributors 7 | http://raizlabs.com/ 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining 10 | a copy of this software and associated documentation files (the 11 | "Software"), to deal in the Software without restriction, including 12 | without limitation the rights to use, copy, modify, merge, publish, 13 | distribute, sublicense, and/or sell copies of the Software, and to 14 | permit persons to whom the Software is furnished to do so, subject to 15 | the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be 18 | included in all copies or substantial portions of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 21 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 22 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 23 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 24 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 25 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 26 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 27 | Generated by CocoaPods - https://cocoapods.org 28 | -------------------------------------------------------------------------------- /Example/Swiftilities/Views/Lifecycle/LifecycleBehaviorsListViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LifecycleBehaviorsListViewController.swift 3 | // Swiftilities 4 | // 5 | // Created by Jason Clark on 5/30/17. 6 | // Copyright © 2017 Raizlabs. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class LifecycleBehaviorsListViewController: UIViewController { 12 | 13 | @IBAction func hairlineDemoSelected() { 14 | let vc = NavBarHairlineFadeDemoViewController() 15 | vc.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .stop, target: self, action: #selector(dismissPressed)) 16 | let nav = UINavigationController(rootViewController: vc) 17 | let navBar = nav.navigationBar 18 | navBar.setBackgroundImage(UIImage(), for: .default) 19 | navBar.shadowImage = UIImage() 20 | navBar.backgroundColor = .white 21 | navBar.isTranslucent = false 22 | present(nav, animated: true, completion: nil) 23 | } 24 | 25 | @IBAction func titleTransitionDemoSelected() { 26 | let vc = NavBarTitleTransitionDemoViewController() 27 | vc.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .stop, target: self, action: #selector(dismissPressed)) 28 | let nav = UINavigationController(rootViewController: vc) 29 | let navBar = nav.navigationBar 30 | navBar.isTranslucent = false 31 | present(nav, animated: true, completion: nil) 32 | } 33 | 34 | @objc func dismissPressed() { 35 | dismiss(animated: true, completion: nil) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Pod/Classes/Lifecycle/Framework/ViewControllerLifecycleBehavior.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewControllerLifecycleBehavior.swift 3 | // Swiftilities 4 | // 5 | // Created by Nicholas Bonatsakis on 6/10/16. 6 | // Copyright© 2016 Raizlabs 7 | // 8 | // Based on concepts and examples in: 9 | // http://khanlou.com/2016/02/many-controllers/ and 10 | // http://irace.me/lifecycle-behaviors 11 | 12 | #if canImport(UIKit) 13 | import UIKit 14 | 15 | public protocol ViewControllerLifecycleBehavior { 16 | 17 | func beforeAppearing(_ viewController: UIViewController, animated: Bool) 18 | 19 | func afterAppearing(_ viewController: UIViewController, animated: Bool) 20 | 21 | func beforeDisappearing(_ viewController: UIViewController, animated: Bool) 22 | 23 | func afterDisappearing(_ viewController: UIViewController, animated: Bool) 24 | 25 | func beforeLayingOutSubviews(_ viewController: UIViewController) 26 | 27 | func afterLayingOutSubviews(_ viewController: UIViewController) 28 | 29 | } 30 | 31 | public extension ViewControllerLifecycleBehavior { 32 | 33 | func beforeAppearing(_ viewController: UIViewController, animated: Bool) {} 34 | 35 | func afterAppearing(_ viewController: UIViewController, animated: Bool) {} 36 | 37 | func beforeDisappearing(_ viewController: UIViewController, animated: Bool) {} 38 | 39 | func afterDisappearing(_ viewController: UIViewController, animated: Bool) {} 40 | 41 | func beforeLayingOutSubviews(_ viewController: UIViewController) {} 42 | 43 | func afterLayingOutSubviews(_ viewController: UIViewController) {} 44 | 45 | } 46 | 47 | #endif 48 | -------------------------------------------------------------------------------- /Pod/Classes/HairlineView/README.md: -------------------------------------------------------------------------------- 1 | # HairlineView 2 | 3 | A `UIView` that has an intrinsic size in one axis (thickness) and is freely resizable in the other. `HairlineView` has a fixed axis, line thickness, and color. 4 | 5 | 6 | ## Default Appearance 7 | 8 |
9 | Screenshots 10 | 11 | 12 | 13 |
14 | 15 | ### Quick Start 16 | 17 | A simple 1 pixel, horizontal, dark gray hairline: 18 | ```swift 19 | let hairline = HairlineView() 20 | cell.contentView.addSubview(hairline) 21 | hairline.translatesAutoresizingMaskIntoConstraints = false 22 | hairline.leadingAnchor.constraint(equalTo: cell.contentView.leadingAnchor).isActive = true 23 | hairline.trailingAnchor.constraint(equalTo: cell.contentView.trailingAnchor).isActive = true 24 | hairline.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor).isActive = true 25 | ``` 26 | 27 | ## Custom Appearance 28 | 29 |
30 | Screenshots 31 | 32 | 33 | 34 |
35 | 36 | To customize, add init parameters for axis, thickness and color. 37 | 38 | ```swift 39 | let hairline = HairlineView(axis: .vertical, thickness: 3.0, hairlineColor: .red) 40 | view.addSubview(hairline) 41 | hairline.translatesAutoresizingMaskIntoConstraints = false 42 | hairline.heightAnchor.constraint(equalToConstant: 12.0).isActive = true 43 | hairline.centerYAnchor.constraint(equalTo: button.centerYAnchor).isActive = true 44 | hairline.leadingAnchor.constraint(equalTo: button.trailingAnchor, constant: 20).isActive = true 45 | ``` 46 | -------------------------------------------------------------------------------- /Example/Swiftilities/SupportingFiles/TestModel.xcdatamodeld/TestModel.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /Pod/Classes/Views/Textview/ExpandingTextView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExpandingTextView.swift 3 | // Swiftilities 4 | // 5 | // Created by Derek Ostrander on 6/8/16. 6 | // Copyright © 2016 Raizlabs. All rights reserved. 7 | // 8 | 9 | #if canImport(UIKit) 10 | import UIKit 11 | 12 | @available(*, unavailable, renamed: "ExpandingTextView") 13 | typealias TailoredSwiftTextView = ExpandingTextView 14 | 15 | open class ExpandingTextView: PlaceholderTextView, HeightAutoAdjustable { 16 | open weak var animationDelegate: TextViewAnimationDelegate? 17 | open var animateHeightChange: Bool = true 18 | open var heightPriority: UILayoutPriority = UILayoutPriority.defaultHigh 19 | 20 | override open var text: String! { 21 | didSet { 22 | layoutIfNeeded() 23 | updateAppearance() 24 | } 25 | } 26 | 27 | override open var attributedText: NSAttributedString! { 28 | didSet { 29 | layoutIfNeeded() 30 | updateAppearance() 31 | } 32 | } 33 | 34 | override public init(frame: CGRect, textContainer: NSTextContainer?) { 35 | super.init(frame: frame, textContainer: textContainer) 36 | adjustHeight() 37 | } 38 | 39 | required public init?(coder aDecoder: NSCoder) { 40 | super.init(coder: aDecoder) 41 | adjustHeight() 42 | } 43 | 44 | override func textDidChange(_ notification: Notification) { 45 | if let object = notification.object as AnyObject?, object === self { 46 | updateAppearance() 47 | } 48 | } 49 | 50 | private func updateAppearance() { 51 | adjustHeight() 52 | adjustPlaceholder() 53 | } 54 | } 55 | 56 | #endif 57 | -------------------------------------------------------------------------------- /Pod/Classes/BetterButton/README.md: -------------------------------------------------------------------------------- 1 | # BetterButton 2 | 3 | BetterButton is a `UIButton` subclass that allows a wider array of button styles than offered by UIKit out of the box. It uses performant drawing to render all content in images, avoiding common pitfalls associated with custom buttons that must override various control states. 4 | 5 | ## Installation 6 | 7 | BetterButton is part of Swiftilities, available through [CocoaPods](http://cocoapods.org). To install 8 | it, simply add the following line to your Podfile: 9 | 10 | ```ruby 11 | pod "Swiftilities/BetterButton" 12 | ``` 13 | ## Usage 14 | 15 | BetterButton is a subclass of `UIButton`. You simply need to use its custom initializer then you can use it as you would a normal `UIButton`: 16 | 17 | ``` 18 | let pillButton = BetterButton( 19 | shape: .pill, 20 | style: .outlineInvert(backgroundColor: .white, foregroundColor: .green) 21 | ) 22 | pillButton.setTitle("Pill (Outline-Invert)", for: .normal) 23 | ``` 24 | If you need to use an image glyph, set the `iconImage` property instead of using the default `setImage:forState:` method of `UIButton`. This will take your single image and configure the button with the appropriate rendered versions for various states. 25 | 26 | ``` 27 | button.iconImage = UIImage(...) 28 | ``` 29 | Finally, BetterButton also supports a loading state. Set the `isLoading` property to `true` and the button content will be replaced with an activity indicator. Set it back to `false` to restore the initial button style. 30 | 31 | ``` 32 | button.isLoading = true 33 | // Do some long-running task 34 | button.isLoading = false 35 | ``` 36 | For more usage examples, `pod try Swiftilities` and run the sample app, then choose "Buttons" from the menu. -------------------------------------------------------------------------------- /Pod/Classes/TintedButton/README.md: -------------------------------------------------------------------------------- 1 | # TintedButton 2 | 3 | A UIButton with a border and default color behavior 4 | 5 | ## Default Appearance 6 | 7 |
8 | Screenshots 9 | 10 | 11 | 12 |
13 | 14 | ### Quick Start 15 | 16 | A TintedButton with default border width and corner radius. The fill and text colors will be swapped when the button is highlighted (the border will continue to use textColor). 17 | ```swift 18 | let tintedButton = TintedButton(fillColor: .white, textColor: .red) 19 | tintedButton.setTitle("Tinted Button", for: .normal) 20 | view.addSubview(tintedButton) 21 | tintedButton.translatesAutoresizingMaskIntoConstraints = false 22 | let guide = view.safeAreaLayoutGuide 23 | tintedButton.bottomAnchor.constraint(equalTo: guide.topAnchor, constant: 20).isActive = true 24 | tintedButton.widthAnchor.constraint(equalTo: guide.widthAnchor, constant: -40).isActive = true 25 | tintedButton.centerXAnchor.constraint(equalTo: guide.centerXAnchor).isActive = true 26 | ``` 27 | 28 | ## Custom Appearance 29 | 30 |
31 | Screenshots 32 | 33 | 34 | 35 |
36 | 37 | A custom border width and corner radius may also be supplied. 38 | 39 | ```swift 40 | let tintedButton = TintedButton(fillColor: .green, textColor: .black, buttonCornerRadius: 10.0, buttonBorderWidth: 4.0) 41 | tintedButton.setTitle("Tinted Button", for: .normal) 42 | view.addSubview(tintedButton) 43 | tintedButton.translatesAutoresizingMaskIntoConstraints = false 44 | let guide = view.safeAreaLayoutGuide 45 | tintedButton.bottomAnchor.constraint(equalTo: guide.topAnchor, constant: 20).isActive = true 46 | tintedButton.widthAnchor.constraint(equalTo: guide.widthAnchor, constant: -40).isActive = true 47 | tintedButton.centerXAnchor.constraint(equalTo: guide.centerXAnchor).isActive = true 48 | ``` 49 | -------------------------------------------------------------------------------- /Pod/Classes/Math/FloatingPoint+Scale.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FloatingPoint+Scale.swift 3 | // Swiftilities 4 | // 5 | // Created by Zev Eisenberg on 4/15/16. 6 | // Copyright © 2016 Raizlabs. All rights reserved. 7 | // 8 | 9 | public extension BinaryFloatingPoint { 10 | 11 | /// Re-maps a number from one range to another. 12 | /// 13 | /// - Parameters: 14 | /// - source: The range to interpret the number as being a part of. 15 | /// - destination: The range to map the number to. 16 | /// - clamped: Whether the result should be clamped to the `to` range. Defaults to `false`. 17 | /// - reversed: whether the output mapping should be revserd, such that 18 | /// as the input increases, the output decreases. Defaults to `false`. 19 | /// - curve: An optional mapping of input percentage to output percentage. Defaults to `nil` 20 | /// - Returns: The input number, scaled from the `from` range to the `to` range. 21 | func scaled(from source: ClosedRange, to destination: ClosedRange, clamped: Bool = false, reversed: Bool = false, curve: CurveProvider? = nil) -> Self { 22 | 23 | let destinationStart = reversed ? destination.upperBound : destination.lowerBound 24 | let destinationEnd = reversed ? destination.lowerBound : destination.upperBound 25 | 26 | // these are broken up to speed up compile time 27 | let value = clamped ? self.clamped(to: source) : self 28 | let selfMinusLower = value - source.lowerBound 29 | let sourceUpperMinusLower = source.upperBound - source.lowerBound 30 | let destinationUpperMinusLower = destinationEnd - destinationStart 31 | 32 | let percentThroughSource = (selfMinusLower / sourceUpperMinusLower) 33 | let curvedPercent = curve?.map(percentThroughSource) ?? percentThroughSource 34 | var result = curvedPercent * destinationUpperMinusLower + destinationStart 35 | result = clamped ? result.clamped(to: destination) : result 36 | 37 | return result 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /Pod/Classes/Views/Textview/README.md: -------------------------------------------------------------------------------- 1 | # PlaceholderTextView 2 | 3 | A UITextView that can show placeholder text when empty 4 | 5 | ## Default Appearance 6 | 7 |
8 | Screenshots 9 | 10 | 11 | 12 |
13 | 14 | ### Quick Start 15 | 16 | A simple TextView with placeholder text: 17 | ```swift 18 | let aTextView = PlaceholderTextView(frame: CGRect(x: 0, y: 0, width: 200, height: 200)) 19 | aTextView.placeholder = "Type Here" 20 | view.addSubview(aTextView) 21 | aTextView.translatesAutoresizingMaskIntoConstraints = false 22 | aTextView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true 23 | aTextView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true 24 | aTextView.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true 25 | aTextView.heightAnchor.constraint(equalToConstant: 100.0).isActive = true 26 | ``` 27 | A text color can be assigned to `.placeholderTextColor`, or fully styled text can be assigned to the `.attributedPlaceholder` property. 28 | 29 | # ExpandingTextView 30 | 31 | A PlaceholderTextView that resizes height to fit text entered 32 | 33 | ## Default Appearance 34 | 35 |
36 | Screenshots 37 | 38 | 39 | 40 |
41 | 42 | ### Quick Start 43 | 44 | A simple TextView with placeholder text: 45 | ```swift 46 | let expandingTextView = ExpandingTextView(frame: CGRect(x: 0, y: 0, width: 200, height: 200)) 47 | expandingTextView.backgroundColor = .red 48 | expandingTextView.textColor = .white 49 | expandingTextView.font = .systemFont(ofSize: 18) 50 | view.addSubview(expandingTextView) 51 | expandingTextView.translatesAutoresizingMaskIntoConstraints = false 52 | expandingTextView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true 53 | expandingTextView.topAnchor.constraint(equalTo: aTextView.bottomAnchor, constant: 20).isActive = true 54 | expandingTextView.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true 55 | ``` 56 | -------------------------------------------------------------------------------- /Pod/Classes/Views/Gradient/README.md: -------------------------------------------------------------------------------- 1 | # GradientView 2 | 3 | A UIView that contains a gradient 4 | 5 | ## Default Appearance 6 | 7 |
8 | Screenshots 9 | 10 | 11 | 12 |
13 | 14 | ### Quick Start 15 | 16 | A linear gradient transitioning from white to blue: 17 | ```swift 18 | let gradientView = GradientView(direction: .leftToRight, colors: [.white, .blue]) 19 | cell.contentView.addSubview(gradientView) 20 | gradientView.translatesAutoresizingMaskIntoConstraints = false 21 | gradientView.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true 22 | gradientView.heightAnchor.constraint(equalToConstant: 64.0).isActive = true 23 | ``` 24 | 25 | ## Custom Appearance 26 | 27 |
28 | Screenshots 29 | 30 | 31 | 32 |
33 | 34 | Direction can be changed or a custom direction can be defined with Floats from 0.0 to 1.0 describing the start and end position within the view. Locations can be provided to define how the color array should be spaced; if no locations are provided, the colors will be evenly spaced between the start and end positions. 35 | 36 | ```swift 37 | let gradients = [ 38 | GradientView(direction: .leftToRight, colors: [.red, .white, .blue]), 39 | GradientView(direction: .topToBottom, colors: [.green, .black, .orange]), 40 | // Gradient starting in top left corner (0,0) and ending in bottom right corner (1,1): 41 | GradientView( 42 | direction: .custom(start: CGPoint(x: 0, y: 0), end: CGPoint(x: 1, y: 1)), 43 | colors: [.blue, .orange, .purple] 44 | ), 45 | // Gradient confined to bottom 10%: 46 | GradientView(direction: .topToBottom, colors: [.purple, .black], locations: [0.9, 1.0]), 47 | ] 48 | gradients.forEach { view in 49 | stackView.addArrangedSubview(view) 50 | view.widthAnchor.constraint(equalTo: stackView.widthAnchor).isActive = true 51 | view.heightAnchor.constraint(equalToConstant: 64.0).isActive = true 52 | view.layer.masksToBounds = true 53 | view.layer.cornerRadius = 6.0 54 | } 55 | ``` 56 | -------------------------------------------------------------------------------- /Pod/Classes/Views/Textview/Protocols.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Protocols.swift 3 | // Swiftilities 4 | // 5 | // Created by Derek Ostrander on 6/8/16. 6 | // Copyright © 2016 Raizlabs. All rights reserved. 7 | // 8 | 9 | #if canImport(UIKit) 10 | import UIKit 11 | 12 | typealias OriginConstraints = (x: NSLayoutConstraint?, y: NSLayoutConstraint?) 13 | 14 | protocol PlaceholderConfigurable { 15 | var placeholderLabel: UILabel { get } 16 | var placeholderPosition: CGPoint { get } 17 | var placeholderConstraints: OriginConstraints { get } 18 | 19 | func adjustPlaceholder() 20 | } 21 | 22 | public protocol TextViewAnimationDelegate: class { 23 | /// Whether or not the given UITextView should animate its height changes. 24 | /// - Parameter textView: The UITextView that should or should not have the changes to its height animated. 25 | /// - Returns: Whether or not changes to the height of the UITextView will be animated. 26 | func shouldAnimateHeightChange(_ textView: UITextView) -> Bool 27 | 28 | /// Gets the container view that will call `setNeedsLayout` in order to animate the given UITextView. Should be a parent of `textView`, but if not, the call will have no effect. Will also have no effect if `shouldAnimateHeightChange` returns false. 29 | /// - Parameter textView: The UITextView whose height is to be changed. 30 | /// - Returns: The UIView responsible for animating layout changes by laying out its subviews. 31 | func containerToLayout(forTextView textView: UITextView) -> UIView? 32 | 33 | /// Gets the animation duration for when `textView` changes height. Value is not used if `shouldAnimateHeightChange` returns false. 34 | /// - Parameter textView: The UITextView whose height is to be changed. 35 | /// - Returns: The duration of the animation, in seconds. 36 | func animationDuration(_ textView: UITextView) -> TimeInterval? 37 | } 38 | 39 | protocol HeightAutoAdjustable { 40 | var animationDelegate: TextViewAnimationDelegate? { get set } 41 | var heightPriority: UILayoutPriority { get } 42 | var intrinsicContentHeight: CGFloat { get } 43 | 44 | func heightConstraint() -> NSLayoutConstraint 45 | func adjustHeight() 46 | } 47 | 48 | #endif 49 | -------------------------------------------------------------------------------- /Example/Tests/RootViewControllerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RootViewControllerTests.swift 3 | // Swiftilities 4 | // 5 | // Created by Michael Skiba on 2/22/17. 6 | // Copyright © 2017 Raizlabs. All rights reserved. 7 | // 8 | 9 | #if canImport(UIKit) 10 | import UIKit 11 | import Swiftilities 12 | import XCTest 13 | 14 | class RootViewControllerTests: XCTestCase { 15 | 16 | var demoWindow: UIWindow = UIWindow() 17 | 18 | override func setUp() { 19 | super.setUp() 20 | demoWindow = UIWindow() 21 | } 22 | 23 | override func tearDown() { 24 | super.tearDown() 25 | } 26 | 27 | func testCompletionAnimatedFromEmpty() { 28 | let expect = expectation(description: "Completion handler should be executed") 29 | let completion = { expect.fulfill() } 30 | demoWindow.setRootViewController(UIViewController(), animated: true, completion: completion) 31 | waitForExpectations(timeout: 1.0, handler: nil) 32 | } 33 | 34 | func testCompletionNonAnimatedFromEmpty() { 35 | let expect = expectation(description: "Fist completion handler should fire") 36 | let completion = { expect.fulfill() } 37 | demoWindow.setRootViewController(UIViewController(), animated: false, completion: completion) 38 | waitForExpectations(timeout: 1.0, handler: nil) 39 | } 40 | 41 | func testCompletionAnimated() { 42 | demoWindow.rootViewController = UIViewController() 43 | let expect = expectation(description: "Completion handler should be executed") 44 | let completion = { expect.fulfill() } 45 | demoWindow.setRootViewController(UIViewController(), animated: true, completion: completion) 46 | waitForExpectations(timeout: 1.0, handler: nil) 47 | } 48 | 49 | func testCompletionNonAnimated() { 50 | demoWindow.rootViewController = UIViewController() 51 | let expect = expectation(description: "Completion handler should be executed") 52 | let completion = { expect.fulfill() } 53 | demoWindow.setRootViewController(UIViewController(), animated: false, completion: completion) 54 | waitForExpectations(timeout: 1.0, handler: nil) 55 | } 56 | 57 | } 58 | 59 | #endif 60 | -------------------------------------------------------------------------------- /Example/Swiftilities/Views/Lifecycle/NavBarHairlineFadeDemoViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NavBarHairlineFadeDemoViewController.swift 3 | // Swiftilities 4 | // 5 | // Created by Jason Clark on 5/26/17. 6 | // Copyright © 2017 Raizlabs. All rights reserved. 7 | // 8 | 9 | import Swiftilities 10 | 11 | final class NavBarHairlineFadeDemoViewController: UIViewController { 12 | 13 | let scrollView = UIScrollView() 14 | let titleLabel = UILabel() 15 | let contentView = UIView() 16 | 17 | override func viewDidLoad() { 18 | super.viewDidLoad() 19 | navigationItem.title = "Nav Bar Hairline Fade" 20 | titleLabel.text = "Scroll View" 21 | view.backgroundColor = .white 22 | let behavior = NavBarHairlineFadeBehavior(scrollView: scrollView) 23 | addBehaviors([behavior]) 24 | } 25 | 26 | } 27 | 28 | extension NavBarHairlineFadeDemoViewController { 29 | 30 | override func loadView() { 31 | view = UIView() 32 | view.addSubview(scrollView) 33 | scrollView.addSubview(contentView) 34 | contentView.addSubview(titleLabel) 35 | for view in [scrollView, titleLabel, contentView] { 36 | view.translatesAutoresizingMaskIntoConstraints = false 37 | } 38 | 39 | NSLayoutConstraint.activate([ 40 | scrollView.topAnchor.constraint(equalTo: view.topAnchor), 41 | scrollView.leftAnchor.constraint(equalTo: view.leftAnchor), 42 | scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor), 43 | scrollView.rightAnchor.constraint(equalTo: view.rightAnchor), 44 | 45 | contentView.topAnchor.constraint(equalTo: scrollView.topAnchor), 46 | contentView.leftAnchor.constraint(equalTo: scrollView.leftAnchor), 47 | contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor), 48 | contentView.rightAnchor.constraint(equalTo: scrollView.rightAnchor), 49 | contentView.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 2), 50 | 51 | titleLabel.centerXAnchor.constraint(equalTo: scrollView.centerXAnchor), 52 | titleLabel.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: 200), 53 | ]) 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /Pod/Classes/Forms/UIView+Lookup.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+Lookup.swift 3 | // Swiftilities 4 | // 5 | // Created by Brian King on 4/6/16. 6 | // Copyright © 2016 Raizlabs. All rights reserved. 7 | // 8 | 9 | #if canImport(UIKit) 10 | import UIKit 11 | 12 | extension UIView { 13 | 14 | /// Lookup the subviews of a specific type, sorted by y position. 15 | /// 16 | /// - parameter includeHidden: Return hidden views in the response. The default is false. 17 | /// - parameter includeNonInteractable: Return views that do not have user interaction enabled. The default is false. 18 | /// 19 | /// - returns: An array of views, of type T, sorted by y position. 20 | @nonobjc final public func lookupSortedViews(includeHidden: Bool = false, includeNonInteractable: Bool = false) -> [T] { 21 | if let view = self as? T { 22 | if isHidden.equals(false, shouldTest: !includeHidden) && 23 | isUserInteractionEnabled.equals(true, shouldTest: !includeNonInteractable) { 24 | return [view] 25 | } 26 | } 27 | var views: [T] = Array() 28 | for subview in subviews { 29 | let results: [T] = subview.lookupSortedViews(includeHidden: includeHidden, 30 | includeNonInteractable: includeNonInteractable) 31 | views.append(contentsOf: results) 32 | } 33 | views.sort { (view, otherView) -> Bool in 34 | let center = convert(view.center, from: view.superview) 35 | let otherCenter = convert(otherView.center, from: otherView.superview) 36 | return center.y < otherCenter.y 37 | } 38 | return views 39 | } 40 | 41 | @nonobjc final public func lookupParentView() -> T? { 42 | var parent: UIView? = superview 43 | while parent != nil && parent as? T == nil { 44 | parent = parent?.superview 45 | } 46 | return parent as? T 47 | } 48 | } 49 | 50 | private extension Bool { 51 | 52 | func equals(_ value: Bool, shouldTest: Bool) -> Bool { 53 | guard shouldTest else { 54 | return true 55 | } 56 | return self == value 57 | } 58 | 59 | } 60 | 61 | #endif 62 | -------------------------------------------------------------------------------- /Example/Swiftilities/Views/Lifecycle/NavBarTitleTransitionDemoViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NavBarTitleTransitionDemoViewController.swift 3 | // Swiftilities 4 | // 5 | // Created by Jason Clark on 5/26/17. 6 | // Copyright © 2017 Raizlabs. All rights reserved. 7 | // 8 | 9 | import Swiftilities 10 | 11 | class NavBarTitleTransitionDemoViewController: UIViewController { 12 | 13 | let scrollView = UIScrollView() 14 | let titleLabel = UILabel() 15 | let contentView = UIView() 16 | 17 | override func viewDidLoad() { 18 | super.viewDidLoad() 19 | navigationItem.title = "Title Transition" 20 | titleLabel.text = "Title Transition" 21 | view.backgroundColor = .lightGray 22 | 23 | let behavior = NavTitleTransitionBehavior(scrollView: scrollView, titleView: titleLabel) 24 | addBehaviors([behavior]) 25 | } 26 | 27 | } 28 | 29 | extension NavBarTitleTransitionDemoViewController { 30 | 31 | override func loadView() { 32 | view = UIView() 33 | view.addSubview(scrollView) 34 | scrollView.addSubview(contentView) 35 | contentView.addSubview(titleLabel) 36 | for view in [scrollView, titleLabel, contentView] { 37 | view.translatesAutoresizingMaskIntoConstraints = false 38 | } 39 | 40 | NSLayoutConstraint.activate([ 41 | scrollView.topAnchor.constraint(equalTo: view.topAnchor), 42 | scrollView.leftAnchor.constraint(equalTo: view.leftAnchor), 43 | scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor), 44 | scrollView.rightAnchor.constraint(equalTo: view.rightAnchor), 45 | 46 | contentView.topAnchor.constraint(equalTo: scrollView.topAnchor), 47 | contentView.leftAnchor.constraint(equalTo: scrollView.leftAnchor), 48 | contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor), 49 | contentView.rightAnchor.constraint(equalTo: scrollView.rightAnchor), 50 | contentView.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 2), 51 | 52 | titleLabel.centerXAnchor.constraint(equalTo: scrollView.centerXAnchor), 53 | titleLabel.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: 200), 54 | ]) 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /Pod/Classes/ColorHelpers/UIColor+Helpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIColor+Helpers.swift 3 | // Swiftilities 4 | // 5 | // Created by Michael Skiba on 11/14/16. 6 | // Copyright © 2016 Raizlabs. All rights reserved. 7 | // 8 | 9 | #if canImport(UIKit) 10 | import UIKit 11 | 12 | public extension UIColor { 13 | 14 | convenience init(hex: UInt32, alpha: CGFloat = 1.0) { 15 | let r = UInt8((hex & 0xFF0000) >> 16) 16 | let g = UInt8((hex & 0xFF00) >> 8) 17 | let b = UInt8(hex & 0xFF) 18 | self.init(red8: r, green8: g, blue8: b, alpha: alpha) 19 | } 20 | 21 | convenience init(rgba: UInt32) { 22 | let r = UInt8((rgba & 0xFF000000) >> 24) 23 | let g = UInt8((rgba & 0xFF0000) >> 16) 24 | let b = UInt8((rgba & 0xFF00) >> 8) 25 | let a = UInt8(rgba & 0xFF) 26 | self.init(red8: r, green8: g, blue8: b, alpha: CGFloat(a) / 255) 27 | } 28 | 29 | convenience init(red8: UInt8, green8: UInt8, blue8: UInt8, alpha: CGFloat = 1.0) { 30 | self.init(red: CGFloat(red8) / 255.0, green: CGFloat(green8) / 255.0, blue: CGFloat(blue8) / 255.0, alpha: alpha) 31 | } 32 | 33 | func lightened(by percentage: CGFloat) -> UIColor { 34 | return self.brightnessAdjusted(by: abs(percentage) ) 35 | } 36 | 37 | func darkened(by percentage: CGFloat) -> UIColor { 38 | return self.brightnessAdjusted(by: -1 * abs(percentage) ) 39 | } 40 | 41 | var averageBrightness: CGFloat { 42 | var r: CGFloat = 0, g: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0 43 | if (self.getRed(&r, green: &g, blue: &b, alpha: &a)) { 44 | return (r + g + b) / 3.0 45 | } 46 | else { 47 | return 1.0 48 | } 49 | 50 | } 51 | 52 | func brightnessAdjusted(by percentage: CGFloat) -> UIColor { 53 | var r: CGFloat = 0, g: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0 54 | if (self.getRed(&r, green: &g, blue: &b, alpha: &a)) { 55 | return UIColor(red: min(r + percentage, 1.0), 56 | green: min(g + percentage, 1.0), 57 | blue: min(b + percentage, 1.0), 58 | alpha: a) 59 | } 60 | else { 61 | return .black 62 | } 63 | } 64 | 65 | } 66 | 67 | #endif 68 | -------------------------------------------------------------------------------- /Pod/Classes/RootViewController/UIWindow+RootViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIWindow+RootViewController.swift 3 | // Swiftilities 4 | // 5 | // Created by Nicholas Bonatsakis on 2/5/16. 6 | // Copyright © 2016 Raizlabs. All rights reserved. 7 | // 8 | 9 | #if canImport(UIKit) 10 | import UIKit 11 | 12 | /** 13 | * UIWindow extension for setting the rootViewController on a UIWindow instance in a safe and animatable way. 14 | */ 15 | public extension UIWindow { 16 | 17 | /** 18 | Set the rootViewController on this UIWindow instance. 19 | 20 | - parameter viewController: The view controller to set 21 | - parameter animated: Whether or not to animate the transition, animation is a cross-fade 22 | - parameter completion: Completion block to be invoked after the transition finishes 23 | */ 24 | @nonobjc func setRootViewController(_ viewController: UIViewController, animated: Bool, completion: @escaping () -> Void = {}) { 25 | let previousRootViewController = rootViewController 26 | let updateViewController = { 27 | // Disabling animation prevents layout and visual issues during the transition 28 | UIView.performWithoutAnimation { 29 | self.rootViewController = viewController 30 | } 31 | } 32 | let removePreviousAndExecuteCompletion = { (_: Bool) in 33 | // If a view controller is currently presented, it must be dismissed as a separate step 34 | // than the swapping of the root VC of the window. 35 | // Failure to do this appears to result in a retain cycle in the orphaned VC stack. 36 | previousRootViewController?.dismiss(animated: false, completion: nil) 37 | previousRootViewController?.view.removeFromSuperview() 38 | completion() 39 | } 40 | if animated && previousRootViewController != nil { 41 | UIView.transition(with: self, 42 | duration: 0.3, 43 | options: .transitionCrossDissolve, 44 | animations: updateViewController, 45 | completion: removePreviousAndExecuteCompletion) 46 | } 47 | else { 48 | updateViewController() 49 | removePreviousAndExecuteCompletion(true) 50 | } 51 | } 52 | } 53 | 54 | #endif 55 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-Swiftilities_Tests/Pods-Swiftilities_Tests-acknowledgements.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreferenceSpecifiers 6 | 7 | 8 | FooterText 9 | This application makes use of the following third party libraries: 10 | Title 11 | Acknowledgements 12 | Type 13 | PSGroupSpecifier 14 | 15 | 16 | FooterText 17 | Copyright 2016 Raizlabs and other contributors 18 | http://raizlabs.com/ 19 | 20 | Permission is hereby granted, free of charge, to any person obtaining 21 | a copy of this software and associated documentation files (the 22 | "Software"), to deal in the Software without restriction, including 23 | without limitation the rights to use, copy, modify, merge, publish, 24 | distribute, sublicense, and/or sell copies of the Software, and to 25 | permit persons to whom the Software is furnished to do so, subject to 26 | the following conditions: 27 | 28 | The above copyright notice and this permission notice shall be 29 | included in all copies or substantial portions of the Software. 30 | 31 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 32 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 33 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 34 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 35 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 36 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 37 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 38 | License 39 | MIT 40 | Title 41 | Swiftilities 42 | Type 43 | PSGroupSpecifier 44 | 45 | 46 | FooterText 47 | Generated by CocoaPods - https://cocoapods.org 48 | Title 49 | 50 | Type 51 | PSGroupSpecifier 52 | 53 | 54 | StringsTable 55 | Acknowledgements 56 | Title 57 | Acknowledgements 58 | 59 | 60 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-Swiftilities_Example/Pods-Swiftilities_Example-acknowledgements.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreferenceSpecifiers 6 | 7 | 8 | FooterText 9 | This application makes use of the following third party libraries: 10 | Title 11 | Acknowledgements 12 | Type 13 | PSGroupSpecifier 14 | 15 | 16 | FooterText 17 | Copyright 2016 Raizlabs and other contributors 18 | http://raizlabs.com/ 19 | 20 | Permission is hereby granted, free of charge, to any person obtaining 21 | a copy of this software and associated documentation files (the 22 | "Software"), to deal in the Software without restriction, including 23 | without limitation the rights to use, copy, modify, merge, publish, 24 | distribute, sublicense, and/or sell copies of the Software, and to 25 | permit persons to whom the Software is furnished to do so, subject to 26 | the following conditions: 27 | 28 | The above copyright notice and this permission notice shall be 29 | included in all copies or substantial portions of the Software. 30 | 31 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 32 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 33 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 34 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 35 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 36 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 37 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 38 | License 39 | MIT 40 | Title 41 | Swiftilities 42 | Type 43 | PSGroupSpecifier 44 | 45 | 46 | FooterText 47 | Generated by CocoaPods - https://cocoapods.org 48 | Title 49 | 50 | Type 51 | PSGroupSpecifier 52 | 53 | 54 | StringsTable 55 | Acknowledgements 56 | Title 57 | Acknowledgements 58 | 59 | 60 | -------------------------------------------------------------------------------- /Pod/Classes/AboutView/AppInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppInfo.swift 3 | // Swiftilities 4 | // 5 | // Created by Michael Skiba on 2/28/17. 6 | // Copyright © 2017 Raizlabs. All rights reserved. 7 | // 8 | 9 | #if canImport(UIKit) 10 | import UIKit 11 | 12 | enum AppInfo { 13 | 14 | private static var unknownKeyString: String { 15 | return NSLocalizedString("Unknown", comment: "Indicates that a field in the app info is unknown") 16 | } 17 | 18 | static var systemVersion: String { 19 | return UIDevice.current.systemVersion 20 | } 21 | 22 | static var name: String { 23 | let bundleName = Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String 24 | return bundleName ?? unknownKeyString 25 | } 26 | 27 | static var version: String { 28 | let bundleVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String 29 | return bundleVersion ?? unknownKeyString 30 | } 31 | 32 | static var accessibleVersion: String { 33 | if UIAccessibility.isVoiceOverRunning { 34 | let separator = NSLocalizedString(" point ", comment: "The spelled out version of the “point” in version numbers, like 2 point 0 point 1, with spaces on either side") 35 | return version.components(separatedBy: ".").joined(separator: separator) 36 | } 37 | else { 38 | return version 39 | } 40 | } 41 | 42 | static var buildNumber: String { 43 | let bundleVersionKey = kCFBundleVersionKey as String 44 | let bundleBuild = Bundle.main.object(forInfoDictionaryKey: bundleVersionKey) as? String 45 | return bundleBuild ?? unknownKeyString 46 | } 47 | 48 | static var infoText: String { 49 | return ["App Version \(version)", "iOS Version: \(systemVersion)"].joined(separator: "\n") 50 | } 51 | 52 | static var deviceModel: String { 53 | // adapted from http://stackoverflow.com/questions/26028918/ios-how-to-determine-the-current-iphone-device-model-in-swift 54 | var systemInfo = utsname() 55 | uname(&systemInfo) 56 | let mirror = Mirror(reflecting: systemInfo.machine) 57 | 58 | var identifier = "" 59 | for element in mirror.children { 60 | if let value = element.value as? Int8, value != 0 { 61 | identifier += String(UnicodeScalar(UInt8(value))) 62 | } 63 | } 64 | return identifier 65 | } 66 | 67 | } 68 | 69 | #endif 70 | -------------------------------------------------------------------------------- /Example/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Swiftilities (0.25.0): 3 | - Swiftilities/All (= 0.25.0) 4 | - Swiftilities/AboutView (0.25.0) 5 | - Swiftilities/AccessibilityHelpers (0.25.0) 6 | - Swiftilities/Acknowledgements (0.25.0): 7 | - Swiftilities/Compatibility 8 | - Swiftilities/Deselection 9 | - Swiftilities/LicenseFormatter 10 | - Swiftilities/All (0.25.0): 11 | - Swiftilities/AboutView 12 | - Swiftilities/AccessibilityHelpers 13 | - Swiftilities/Acknowledgements 14 | - Swiftilities/BetterButton 15 | - Swiftilities/ColorHelpers 16 | - Swiftilities/Compatibility 17 | - Swiftilities/CoreDataStack 18 | - Swiftilities/Deselection 19 | - Swiftilities/DeviceSize 20 | - Swiftilities/FormattedTextField 21 | - Swiftilities/Forms 22 | - Swiftilities/HairlineView 23 | - Swiftilities/ImageHelpers 24 | - Swiftilities/Keyboard 25 | - Swiftilities/LicenseFormatter 26 | - Swiftilities/Lifecycle 27 | - Swiftilities/Logging 28 | - Swiftilities/Math 29 | - Swiftilities/RootViewController 30 | - Swiftilities/Shapes 31 | - Swiftilities/StackViewHelpers 32 | - Swiftilities/TableViewHelpers 33 | - Swiftilities/TintedButton 34 | - Swiftilities/Views 35 | - Swiftilities/BetterButton (0.25.0): 36 | - Swiftilities/ColorHelpers 37 | - Swiftilities/ImageHelpers 38 | - Swiftilities/Math 39 | - Swiftilities/Shapes 40 | - Swiftilities/ColorHelpers (0.25.0) 41 | - Swiftilities/Compatibility (0.25.0) 42 | - Swiftilities/CoreDataStack (0.25.0) 43 | - Swiftilities/Deselection (0.25.0) 44 | - Swiftilities/DeviceSize (0.25.0) 45 | - Swiftilities/FormattedTextField (0.25.0) 46 | - Swiftilities/Forms (0.25.0) 47 | - Swiftilities/HairlineView (0.25.0) 48 | - Swiftilities/ImageHelpers (0.25.0) 49 | - Swiftilities/Keyboard (0.25.0) 50 | - Swiftilities/LicenseFormatter (0.25.0) 51 | - Swiftilities/Lifecycle (0.25.0): 52 | - Swiftilities/HairlineView 53 | - Swiftilities/Math 54 | - Swiftilities/Logging (0.25.0) 55 | - Swiftilities/Math (0.25.0) 56 | - Swiftilities/RootViewController (0.25.0) 57 | - Swiftilities/Shapes (0.25.0) 58 | - Swiftilities/StackViewHelpers (0.25.0) 59 | - Swiftilities/TableViewHelpers (0.25.0) 60 | - Swiftilities/TintedButton (0.25.0) 61 | - Swiftilities/Views (0.25.0) 62 | 63 | DEPENDENCIES: 64 | - Swiftilities (from `../`) 65 | 66 | EXTERNAL SOURCES: 67 | Swiftilities: 68 | :path: "../" 69 | 70 | SPEC CHECKSUMS: 71 | Swiftilities: 219eb73f3c1d4e95812fc61d3527a5504444a405 72 | 73 | PODFILE CHECKSUM: b6594e5dd1cd2176ee788e0bff997fb286f8d123 74 | 75 | COCOAPODS: 1.8.4 76 | -------------------------------------------------------------------------------- /Example/Pods/Manifest.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Swiftilities (0.25.0): 3 | - Swiftilities/All (= 0.25.0) 4 | - Swiftilities/AboutView (0.25.0) 5 | - Swiftilities/AccessibilityHelpers (0.25.0) 6 | - Swiftilities/Acknowledgements (0.25.0): 7 | - Swiftilities/Compatibility 8 | - Swiftilities/Deselection 9 | - Swiftilities/LicenseFormatter 10 | - Swiftilities/All (0.25.0): 11 | - Swiftilities/AboutView 12 | - Swiftilities/AccessibilityHelpers 13 | - Swiftilities/Acknowledgements 14 | - Swiftilities/BetterButton 15 | - Swiftilities/ColorHelpers 16 | - Swiftilities/Compatibility 17 | - Swiftilities/CoreDataStack 18 | - Swiftilities/Deselection 19 | - Swiftilities/DeviceSize 20 | - Swiftilities/FormattedTextField 21 | - Swiftilities/Forms 22 | - Swiftilities/HairlineView 23 | - Swiftilities/ImageHelpers 24 | - Swiftilities/Keyboard 25 | - Swiftilities/LicenseFormatter 26 | - Swiftilities/Lifecycle 27 | - Swiftilities/Logging 28 | - Swiftilities/Math 29 | - Swiftilities/RootViewController 30 | - Swiftilities/Shapes 31 | - Swiftilities/StackViewHelpers 32 | - Swiftilities/TableViewHelpers 33 | - Swiftilities/TintedButton 34 | - Swiftilities/Views 35 | - Swiftilities/BetterButton (0.25.0): 36 | - Swiftilities/ColorHelpers 37 | - Swiftilities/ImageHelpers 38 | - Swiftilities/Math 39 | - Swiftilities/Shapes 40 | - Swiftilities/ColorHelpers (0.25.0) 41 | - Swiftilities/Compatibility (0.25.0) 42 | - Swiftilities/CoreDataStack (0.25.0) 43 | - Swiftilities/Deselection (0.25.0) 44 | - Swiftilities/DeviceSize (0.25.0) 45 | - Swiftilities/FormattedTextField (0.25.0) 46 | - Swiftilities/Forms (0.25.0) 47 | - Swiftilities/HairlineView (0.25.0) 48 | - Swiftilities/ImageHelpers (0.25.0) 49 | - Swiftilities/Keyboard (0.25.0) 50 | - Swiftilities/LicenseFormatter (0.25.0) 51 | - Swiftilities/Lifecycle (0.25.0): 52 | - Swiftilities/HairlineView 53 | - Swiftilities/Math 54 | - Swiftilities/Logging (0.25.0) 55 | - Swiftilities/Math (0.25.0) 56 | - Swiftilities/RootViewController (0.25.0) 57 | - Swiftilities/Shapes (0.25.0) 58 | - Swiftilities/StackViewHelpers (0.25.0) 59 | - Swiftilities/TableViewHelpers (0.25.0) 60 | - Swiftilities/TintedButton (0.25.0) 61 | - Swiftilities/Views (0.25.0) 62 | 63 | DEPENDENCIES: 64 | - Swiftilities (from `../`) 65 | 66 | EXTERNAL SOURCES: 67 | Swiftilities: 68 | :path: "../" 69 | 70 | SPEC CHECKSUMS: 71 | Swiftilities: 219eb73f3c1d4e95812fc61d3527a5504444a405 72 | 73 | PODFILE CHECKSUM: b6594e5dd1cd2176ee788e0bff997fb286f8d123 74 | 75 | COCOAPODS: 1.8.4 76 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | jobs: 4 | test: 5 | executor: xcode-12 6 | steps: 7 | - setup 8 | - run: swift test 9 | - run: bundle exec pod lib lint --swift-version=4.2 10 | - run: bundle exec pod lib lint --swift-version=5.0 11 | - run: 12 | name: Fastlane Test 13 | command: | 14 | cd Example 15 | bundle exec fastlane test 16 | 17 | carthage-build: 18 | executor: xcode-12 19 | steps: 20 | - checkout # We can skip the Ruby gem restoration for Carthage 21 | - run: 22 | name: Update homebrew dependencies 23 | command: brew update 1> /dev/null 2> /dev/null 24 | - run: 25 | name: Update Carthage 26 | command: brew outdated carthage || (brew uninstall carthage --force; HOMEBREW_NO_AUTO_UPDATE=1 brew install carthage --force-bottle) 27 | # Carthage does not work on Xcode 12 https://github.com/Carthage/Carthage/issues/3019 28 | # - run: 29 | # name: Build with Carthage 30 | # command: carthage build --no-skip-current && test -d Carthage/Build/iOS/Swiftilities.framework 31 | 32 | deploy-to-cocoapods: 33 | executor: xcode-12 34 | steps: 35 | - setup 36 | - run: bundle exec pod trunk push 37 | 38 | executors: 39 | xcode-12: 40 | macos: 41 | xcode: "12.1.0" 42 | environment: 43 | LC_ALL: en_US.UTF-8 44 | LANG: en_US.UTF-8 45 | shell: /bin/bash --login -eo pipefail 46 | 47 | commands: 48 | setup: 49 | description: "Shared setup" 50 | steps: 51 | - checkout 52 | - restore-gems 53 | 54 | restore-gems: 55 | description: "Restore Ruby Gems" 56 | steps: 57 | - run: 58 | name: Set Ruby Version 59 | command: echo "ruby-2.5" > ~/.ruby-version 60 | - restore_cache: 61 | key: 1-gems-{{ checksum "Gemfile.lock" }} 62 | - run: bundle check || bundle install --path vendor/bundle 63 | - save_cache: 64 | key: 1-gems-{{ checksum "Gemfile.lock" }} 65 | paths: 66 | - vendor/bundle 67 | 68 | workflows: 69 | version: 2 70 | build-test-deploy: 71 | jobs: 72 | - test: 73 | filters: 74 | tags: 75 | only: /.*/ 76 | - carthage-build: 77 | filters: 78 | tags: 79 | only: /.*/ 80 | - deploy-to-cocoapods: 81 | context: CocoaPods 82 | requires: 83 | - test 84 | - carthage-build 85 | filters: 86 | tags: 87 | only: /\d+(\.\d+)*(-.*)*/ 88 | branches: 89 | ignore: /.*/ 90 | -------------------------------------------------------------------------------- /Example/Swiftilities/Views/Main/MainScreenViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainScreenViewController.swift 3 | // Swiftilities 4 | // 5 | // Created by Michael Skiba on 11/21/16. 6 | // Copyright © 2016 Raizlabs. All rights reserved. 7 | // 8 | 9 | import Swiftilities 10 | import UIKit 11 | 12 | class MainScreenViewController: UIViewController { 13 | 14 | override func viewDidLoad() { 15 | super.viewDidLoad() 16 | 17 | // Do any additional setup after loading the view. 18 | } 19 | 20 | override func didReceiveMemoryWarning() { 21 | super.didReceiveMemoryWarning() 22 | // Dispose of any resources that can be recreated. 23 | } 24 | 25 | @IBAction func showAcknowledgements(_ sender: Any) { 26 | do { 27 | let viewModel = try AcknowledgementsListViewModel(plistNamed: "Pods-Swiftilities_Example-acknowledgements") 28 | let viewController = LightGrayListViewController(viewModel: viewModel) 29 | viewController.childViewControllerClass = LightGrayAcknowledgement.self 30 | navigationController?.pushViewController(viewController, animated: true) 31 | } 32 | catch { 33 | fatalError("Failed to load acknowledgements") 34 | } 35 | } 36 | 37 | @IBAction func demonstrateRootViewControllerCycling(_ sender: Any) { 38 | let v1 = UIViewController() 39 | v1.view.backgroundColor = .red 40 | let v2 = UIViewController() 41 | v2.view.backgroundColor = .blue 42 | guard let v3 = storyboard?.instantiateInitialViewController(), 43 | let window = UIApplication.shared.keyWindow else { 44 | return 45 | } 46 | present(v1, animated: true) { 47 | window.setRootViewController(v2, animated: true) { 48 | window.setRootViewController(v3, animated: true) 49 | } 50 | } 51 | } 52 | 53 | } 54 | 55 | private class LightGrayListViewController: AcknowledgementsListViewController { 56 | 57 | override func viewDidLoad() { 58 | super.viewDidLoad() 59 | view.backgroundColor = UIColor(rgba: 0xDDDDDDFF) 60 | } 61 | 62 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 63 | let cell = super.tableView(tableView, cellForRowAt: indexPath) 64 | cell.backgroundColor = UIColor(rgba: 0xDDDDDDFF) 65 | return cell 66 | } 67 | } 68 | 69 | private class LightGrayAcknowledgement: AcknowledgementViewController { 70 | 71 | override func loadView() { 72 | super.loadView() 73 | view.backgroundColor = UIColor(rgba: 0xDDDDDDFF) 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /Pod/Classes/Deselection/UIViewController+Deselection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewController+Deselection.swift 3 | // Swiftilities 4 | // 5 | // Created by Zev Eisenberg on 5/13/16. 6 | // Copyright © 2016 Raizlabs. All rights reserved. 7 | // 8 | 9 | #if canImport(UIKit) 10 | import UIKit 11 | 12 | public protocol SmoothlyDeselectableItems { 13 | var indexPathsForSelectedItems: [IndexPath]? { get } 14 | func selectItem(at indexPath: IndexPath?, animated: Bool) 15 | func deselectItem(at indexPath: IndexPath, animated: Bool) 16 | } 17 | 18 | extension UITableView: SmoothlyDeselectableItems { 19 | @nonobjc public var indexPathsForSelectedItems: [IndexPath]? { return indexPathsForSelectedRows } 20 | 21 | @nonobjc public func selectItem(at indexPath: IndexPath?, animated: Bool) { 22 | selectRow(at: indexPath, animated: animated, scrollPosition: .none) 23 | } 24 | 25 | @nonobjc public func deselectItem(at indexPath: IndexPath, animated: Bool) { 26 | deselectRow(at: indexPath, animated: animated) 27 | } 28 | } 29 | 30 | extension UICollectionView: SmoothlyDeselectableItems { 31 | @nonobjc public func selectItem(at indexPath: IndexPath?, animated: Bool) { 32 | selectItem(at: indexPath, animated: animated, scrollPosition: UICollectionView.ScrollPosition()) 33 | } 34 | } 35 | 36 | public extension UIViewController { 37 | 38 | /// Smoothly deselect selected rows in a table view during an animated 39 | /// transition, and intelligently reselect those rows if the interactive 40 | /// transition is canceled. Call this method from inside your view 41 | /// controller's `viewWillAppear(_:)` method. 42 | /// 43 | /// - parameter deselectable: The (de)selectable view in which to perform deselection/reselection. 44 | @nonobjc func smoothlyDeselectItems(_ deselectable: SmoothlyDeselectableItems?) { 45 | let selectedIndexPaths = deselectable?.indexPathsForSelectedItems ?? [] 46 | 47 | if let coordinator = transitionCoordinator { 48 | coordinator.animate(alongsideTransition: { context in 49 | for indexPath in selectedIndexPaths { 50 | deselectable?.deselectItem(at: indexPath, animated: context.isAnimated) 51 | } 52 | }) { context in 53 | if context.isCancelled { 54 | selectedIndexPaths.forEach { 55 | deselectable?.selectItem(at: $0, animated: false) 56 | } 57 | } 58 | } 59 | } 60 | else { 61 | for indexPath in selectedIndexPaths { 62 | deselectable?.deselectItem(at: indexPath, animated: false) 63 | } 64 | } 65 | } 66 | } 67 | 68 | #endif 69 | -------------------------------------------------------------------------------- /Example/Swiftilities/Views/ShapesViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShapesViewController.swift 3 | // Swiftilities 4 | // 5 | // Created by Nicholas Bonatsakis on 5/9/17. 6 | // Copyright © 2017 Raizlabs. All rights reserved. 7 | // 8 | 9 | import Swiftilities 10 | import UIKit 11 | 12 | class ShapesViewController: UIViewController { 13 | 14 | @IBOutlet weak var stackView: UIStackView! 15 | 16 | override func viewDidLoad() { 17 | super.viewDidLoad() 18 | 19 | let images = [ 20 | Shapes.image(for: .rectangle(cornerRadius: 8), 21 | size: CGSize(width: 300, height: 64), 22 | attributes: 23 | .fillColor(.lightGray), 24 | .lineWidth(2), 25 | .strokeColor(.blue) 26 | ), 27 | Shapes.image(for: .pill, 28 | size: CGSize(width: 300, height: 64), 29 | attributes: 30 | .fillColor(.clear), 31 | .lineWidth(1), 32 | .strokeColor(.orange) 33 | ), 34 | Shapes.image(for: .circle, 35 | size: CGSize(width: 100, height: 100), 36 | attributes: 37 | .fillColor(.clear), 38 | .lineWidth(5), 39 | .strokeColor(.green) 40 | ), 41 | Shapes.image(for: .rectangle(cornerRadius: 8), 42 | size: CGSize(width: 300, height: 64), 43 | attributes: 44 | .fillColor(.lightGray), 45 | .lineWidth(2), 46 | .strokeColor(.blue) 47 | ), 48 | ] 49 | 50 | images.forEach { (image) in 51 | let rectImageView = UIImageView(image: image) 52 | stackView.addArrangedSubview(rectImageView) 53 | } 54 | 55 | let layerSize = CGSize(width: 300, height: 44) 56 | let shapeLayer = Shapes.layer(for: .rectangle(cornerRadius: 8), 57 | size: layerSize, 58 | attributes: 59 | .fillColor(.darkGray), 60 | .lineWidth(4), 61 | .strokeColor(.red) 62 | ) 63 | let shapeView = UIView(frame: CGRect(origin: .zero, size: layerSize)) 64 | shapeView.widthAnchor.constraint(equalToConstant: layerSize.width).isActive = true 65 | shapeView.heightAnchor.constraint(equalToConstant: layerSize.height).isActive = true 66 | shapeView.layer.addSublayer(shapeLayer) 67 | stackView.addArrangedSubview(shapeView) 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /Pod/Classes/Math/CurveProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CurveProvider.swift 3 | // Pods 4 | // 5 | // Created by Jason Clark on 8/10/17. 6 | // 7 | // 8 | 9 | import CoreGraphics.CGBase 10 | 11 | public protocol CurveProvider { 12 | 13 | func map(_ inputPercent: T) -> T 14 | 15 | } 16 | 17 | #if os(iOS) 18 | import UIKit 19 | @available(iOS 10.0, *) 20 | extension UICubicTimingParameters: CurveProvider { 21 | 22 | public func map(_ inputPercent: T) -> T { 23 | guard animationCurve != .linear else { return inputPercent } 24 | return T(CubicBezier.value(for: CGFloat(inputPercent.doubleValue), 25 | controlPoint1: controlPoint1, 26 | controlPoint2: controlPoint2).doubleValue) 27 | } 28 | 29 | } 30 | 31 | extension UIView.AnimationCurve: CurveProvider { 32 | 33 | public func map(_ inputPercent: T) -> T { 34 | guard self != .linear else { return inputPercent } 35 | if #available(iOS 10.0, *) { 36 | return UICubicTimingParameters(animationCurve: self).map(inputPercent) 37 | } 38 | else { 39 | let controlPoint1, controlPoint2: CGPoint 40 | switch self { 41 | case .linear: 42 | controlPoint1 = CGPoint(x: 0, y: 0) 43 | controlPoint2 = CGPoint(x: 1, y: 1) 44 | case .easeIn: 45 | controlPoint1 = CGPoint(x: 0.42, y: 0) 46 | controlPoint2 = CGPoint(x: 1, y: 1) 47 | case .easeOut: 48 | controlPoint1 = CGPoint(x: 0, y: 0) 49 | controlPoint2 = CGPoint(x: 0.58, y: 1) 50 | case .easeInOut: 51 | controlPoint1 = CGPoint(x: 0.42, y: 0) 52 | controlPoint2 = CGPoint(x: 0.58, y: 1) 53 | #if swift(>=5.0) 54 | @unknown default: 55 | debugPrint("ERROR: Unhandled UIView.AnimationCurve case \(self)!") 56 | controlPoint1 = .zero 57 | controlPoint2 = .zero 58 | #endif 59 | } 60 | return T(CubicBezier.value(for: CGFloat(inputPercent.doubleValue), 61 | controlPoint1: controlPoint1, 62 | controlPoint2: controlPoint2).doubleValue) 63 | 64 | } 65 | } 66 | 67 | } 68 | #endif 69 | 70 | extension BinaryFloatingPoint { 71 | 72 | var doubleValue: Double { 73 | switch self { 74 | case let value as Double: return value 75 | case let value as Float: return Double(value) 76 | case let value as CGFloat: return Double(value) 77 | default: fatalError("Unsupported floating point type") 78 | } 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /Pod/Classes/LicenseFormatter/LicenseFormatter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LicenseFormatter.swift 3 | // Swiftilities 4 | // 5 | // Created by Zev Eisenberg on 11/18/16. 6 | // Copyright © 2016 Raizlabs. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension String { 12 | 13 | @nonobjc public var cleanedUpLicense: String { 14 | // add an extra new line between list items 15 | return self.newlinePaddedLists.collapsedArtificialLineBreaks.collapsedDoubleNewLines 16 | } 17 | 18 | } 19 | 20 | internal extension String { 21 | 22 | @nonobjc var newlinePaddedLists: String { 23 | let lineBreakRegexString = "(?<=[^\\n\\r])[\\n\\r][^\\S\\r\\n]*([(?:\\d.)\\*•-])" 24 | 25 | let output: String 26 | do { 27 | let regex = try NSRegularExpression(pattern: lineBreakRegexString, options: []) 28 | output = regex.stringByReplacingMatches(in: self, options: [], range: NSRange(location: 0, length: count), withTemplate: "\n\n$1") 29 | } 30 | catch(let error) { 31 | preconditionFailure("Invalid regular expression string: '\(lineBreakRegexString)', error: \(error)") 32 | } 33 | 34 | return output 35 | } 36 | 37 | @nonobjc var collapsedArtificialLineBreaks: String { 38 | // (?<=\\S): look-behind assertion: non-whitespace character 39 | // \\n: a new line 40 | // [^\\S\\r\\n]: none of: non-whitespace, carriage return, new line. Matches all horizontal whitespace. 41 | // *: the previous set zero or more times (i.e. leading indentation on the line) 42 | // (?=\\S): look-ahead assertion: non-whitespace character 43 | let lineBreakRegexString = "(?<=\\S)\\n[^\\S\\r\\n]*(?=\\S)" 44 | 45 | let output: String 46 | do { 47 | let regex = try NSRegularExpression(pattern: lineBreakRegexString, options: []) 48 | output = regex.stringByReplacingMatches(in: self, options: [], range: NSRange(location: 0, length: count), withTemplate: " ") 49 | } 50 | catch(let error) { 51 | preconditionFailure("Invalid regular expression string: '\(lineBreakRegexString)', error: \(error)") 52 | } 53 | 54 | return output 55 | } 56 | 57 | @nonobjc var collapsedDoubleNewLines: String { 58 | let doubleNewlineRegexString = "([\n\r])[\n\r]" 59 | 60 | let output: String 61 | do { 62 | let regex = try NSRegularExpression(pattern: doubleNewlineRegexString, options: []) 63 | output = regex.stringByReplacingMatches(in: self, options: [], range: NSRange(location: 0, length: count), withTemplate: "$1") 64 | } 65 | catch(let error) { 66 | preconditionFailure("Invalid regular expression string: '\(doubleNewlineRegexString)', error: \(error)") 67 | } 68 | 69 | return output 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /Pod/Classes/ImageHelpers/UIImage+Tinting.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImage+Tinting.swift 3 | // Swiftilities 4 | // 5 | // Created by Nick Bonatsakis on 5/1/17. 6 | // Copyright © 2017 Raizlabs. All rights reserved. 7 | // 8 | 9 | #if canImport(UIKit) 10 | import Foundation 11 | import UIKit 12 | 13 | // Originally sourced from BonMot: https://github.com/Raizlabs/BonMot/blob/master/Sources/Image%2BTinting.swift 14 | // Please apply any improvments made here to source. 15 | public extension UIImage { 16 | 17 | /// Returns a copy of the receiver where the alpha channel is maintained, 18 | /// but every pixel's color is replaced with `color`. 19 | /// 20 | /// - note: The returned image does _not_ have the template flag set, 21 | /// preventing further tinting. 22 | /// 23 | /// - Parameter theColor: The color to use to tint the receiver. 24 | /// - Returns: A tinted copy of the image. 25 | func rz_tintedImage(color theColor: UIColor) -> UIImage { 26 | let imageRect = CGRect(origin: .zero, size: size) 27 | // Save original properties 28 | let originalCapInsets = capInsets 29 | let originalResizingMode = resizingMode 30 | let originalAlignmentRectInsets = alignmentRectInsets 31 | 32 | UIGraphicsBeginImageContextWithOptions(size, false, scale) 33 | guard let context = UIGraphicsGetCurrentContext() else { 34 | return self 35 | } 36 | 37 | // Flip the context vertically 38 | context.translateBy(x: 0.0, y: size.height) 39 | context.scaleBy(x: 1.0, y: -1.0) 40 | 41 | // Image tinting mostly inspired by http://stackoverflow.com/a/22528426/255489 42 | context.setBlendMode(.normal) 43 | context.draw(cgImage!, in: imageRect) 44 | 45 | // .sourceIn: resulting color = source color * destination alpha 46 | context.setBlendMode(.sourceIn) 47 | context.setFillColor(theColor.cgColor) 48 | context.fill(imageRect) 49 | 50 | // Get new image 51 | guard var image = UIGraphicsGetImageFromCurrentImageContext() else { 52 | return self 53 | } 54 | UIGraphicsEndImageContext() 55 | 56 | // Prevent further tinting 57 | image = image.withRenderingMode(.alwaysOriginal) 58 | 59 | // Restore original properties 60 | image = image.withAlignmentRectInsets(originalAlignmentRectInsets) 61 | if originalCapInsets != image.capInsets || originalResizingMode != image.resizingMode { 62 | image = image.resizableImage(withCapInsets: originalCapInsets, resizingMode: originalResizingMode) 63 | } 64 | 65 | // Transfer accessibility label (watchOS not included; does not have accessibilityLabel on UIImage). 66 | image.accessibilityLabel = self.accessibilityLabel 67 | 68 | return image 69 | } 70 | 71 | } 72 | 73 | #endif 74 | -------------------------------------------------------------------------------- /Pod/Classes/Views/Textview/PlaceholderTextView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlaceholderTextView.swift 3 | // Swiftilities 4 | // 5 | // Created by Derek Ostrander on 6/8/16. 6 | // Copyright © 2016 Raizlabs. All rights reserved. 7 | // 8 | 9 | #if canImport(UIKit) 10 | import UIKit 11 | 12 | open class PlaceholderTextView: UITextView, PlaceholderConfigurable { 13 | 14 | let placeholderLabel: UILabel = { 15 | let placeholderLabel = UILabel() 16 | placeholderLabel.translatesAutoresizingMaskIntoConstraints = false 17 | placeholderLabel.textColor = .lightGray 18 | placeholderLabel.font = UIFont.systemFont(ofSize: UIFont.systemFontSize) 19 | return placeholderLabel 20 | }() 21 | 22 | open var placeholder: String? = nil { 23 | didSet { 24 | placeholderLabel.text = placeholder 25 | } 26 | } 27 | 28 | open var attributedPlaceholder: NSAttributedString? = nil { 29 | didSet { 30 | placeholderLabel.attributedText = attributedPlaceholder 31 | } 32 | } 33 | 34 | open var placeholderTextColor: UIColor? = .lightGray { 35 | didSet { 36 | placeholderLabel.textColor = placeholderTextColor 37 | } 38 | } 39 | 40 | override open var textContainerInset: UIEdgeInsets { 41 | didSet { 42 | adjustPlaceholder() 43 | } 44 | } 45 | 46 | override open var text: String! { 47 | didSet { 48 | adjustPlaceholder() 49 | } 50 | } 51 | 52 | override public init(frame: CGRect, textContainer: NSTextContainer?) { 53 | super.init(frame: frame, textContainer: textContainer) 54 | configureTextView() 55 | } 56 | 57 | required public init?(coder aDecoder: NSCoder) { 58 | super.init(coder: aDecoder) 59 | configureTextView() 60 | } 61 | 62 | deinit { 63 | NotificationCenter.default.removeObserver(self, 64 | name: UITextView.textDidChangeNotification, 65 | object: nil) 66 | } 67 | 68 | fileprivate func configureTextView() { 69 | translatesAutoresizingMaskIntoConstraints = false 70 | font = UIFont.systemFont(ofSize: UIFont.systemFontSize) 71 | addSubview(placeholderLabel) 72 | adjustPlaceholder() 73 | NotificationCenter.default.addObserver(self, 74 | selector: #selector(textDidChange), 75 | name:UITextView.textDidChangeNotification, 76 | object: nil) 77 | } 78 | 79 | @objc func textDidChange(_ notification: Notification) { 80 | if let object = notification.object as AnyObject?, object === self { 81 | adjustPlaceholder() 82 | } 83 | } 84 | } 85 | 86 | #endif 87 | -------------------------------------------------------------------------------- /Example/Tests/LicenseFormatterTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LicenseFormatterTests.swift 3 | // Swiftilities 4 | // 5 | // Created by Zev Eisenberg on 11/18/16. 6 | // Copyright © 2016 Raizlabs. All rights reserved. 7 | // 8 | 9 | @testable import Swiftilities 10 | import XCTest 11 | 12 | class StringTest: XCTestCase { 13 | 14 | func testNewLinePadding() { 15 | let originalString = "line 1\n" + 16 | "line 2\n" + 17 | "\n" + 18 | "line 3\n" + 19 | "\n" + 20 | "1. a list with\n" + 21 | " indented stuff\n" + 22 | "2. another list with\n" + 23 | " more indented stuff" 24 | 25 | let controlString = "line 1\n" + 26 | "line 2\n" + 27 | "\n" + 28 | "line 3\n" + 29 | "\n" + 30 | "1. a list with\n" + 31 | " indented stuff\n" + 32 | "\n" + 33 | "2. another list with\n" + 34 | " more indented stuff" 35 | 36 | let testString = originalString.newlinePaddedLists 37 | 38 | XCTAssertEqual(testString, controlString) 39 | } 40 | 41 | func testCollapseArtificialLineBreaks() { 42 | let originalString = "line 1\nline 2\n\nline 3\n\n1. a list with\n indented stuff" 43 | let controlString = "line 1 line 2\n\nline 3\n\n1. a list with indented stuff" 44 | 45 | let testString = originalString.collapsedArtificialLineBreaks 46 | 47 | XCTAssertEqual(testString, controlString) 48 | } 49 | 50 | func testCollapseDoubleNewlines() { 51 | let originalString = "line 1 line 2\n\nline 3\n\n1. a list with indented stuff\n\n\nand a triple" 52 | let controlString = "line 1 line 2\nline 3\n1. a list with indented stuff\n\nand a triple" 53 | 54 | let testString = originalString.collapsedDoubleNewLines 55 | 56 | XCTAssertEqual(testString, controlString) 57 | } 58 | 59 | func testLicenceTidying() { 60 | let originalString = "line 1\nline 2\n\nline 3\n\n1. a list with\n indented stuff\n2. another list with\n more indented stuff" 61 | let controlString = "line 1 line 2\nline 3\n1. a list with indented stuff\n2. another list with more indented stuff" 62 | 63 | let testString = originalString.cleanedUpLicense 64 | 65 | XCTAssertEqual(testString, controlString) 66 | } 67 | 68 | func testLicenceTidyingWithAlternateBulletPoints() { 69 | let originalString = "line 1\nline 2\n\nline 3\n\n1 a list with\n indented stuff\n- another list with\n more indented stuff\n• another bulleted item\n* and another\n123 one more" 70 | let controlString = "line 1 line 2\nline 3\n1 a list with indented stuff\n- another list with more indented stuff\n• another bulleted item\n* and another\n123 one more" 71 | 72 | let testString = originalString.cleanedUpLicense 73 | 74 | XCTAssertEqual(testString, controlString) 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /Pod/Classes/FormattedTextField/FormattedTextField.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FormattedTextField.swift 3 | // Swiftilities 4 | // 5 | // Created by Rob Visentin on 8/9/16. 6 | // Copyright © 2016 Raizlabs. All rights reserved. 7 | // 8 | 9 | #if canImport(UIKit) 10 | import UIKit 11 | 12 | open class FormattedTextField: UITextField { 13 | 14 | public typealias Formatter = (String?) -> String? 15 | 16 | /* 17 | An optional formatter that transforms any text values set set for the text field into the new value 18 | Because this is a closure, it cannot be encoded with NSCoding and must be restored manually if 19 | state restoration is used. 20 | */ 21 | public var formatter: Formatter? { 22 | didSet { 23 | removeTarget(self, action: #selector(textChanged), for: .editingChanged) 24 | if let formatter = formatter { 25 | super.text = formatter(text) 26 | addTarget(self, action: #selector(textChanged), for: .editingChanged) 27 | } 28 | } 29 | } 30 | 31 | open override var text: String? { 32 | didSet { 33 | if let formatter = formatter { 34 | super.text = formatter(text) 35 | } 36 | } 37 | } 38 | 39 | /** 40 | A convenience initializer that allows setting the formatter on init() 41 | 42 | - parameter formatter: The string tranfromation to apply to the text set for this control. 43 | - parameter frame: The frame of the view, default value is CGRect.zero 44 | */ 45 | public convenience init(formatter: Formatter?, frame: CGRect = .zero) { 46 | self.init(frame: frame) 47 | self.formatter = formatter 48 | if formatter != nil { 49 | addTarget(self, action: #selector(textChanged), for: .editingChanged) 50 | } 51 | } 52 | 53 | } 54 | 55 | // MARK: - Private 56 | 57 | private extension FormattedTextField { 58 | 59 | typealias TextFieldState = (length: Int, selectedRange: UITextRange?) 60 | 61 | func saveTextFieldState() -> TextFieldState { 62 | let savedLength = text?.count ?? 0 63 | return (length: savedLength, selectedRange: selectedTextRange) 64 | } 65 | 66 | func restoreTextField(to state: TextFieldState) { 67 | if let savedRange = state.selectedRange, let text = text { 68 | let newLen = text.count 69 | let diff = max(0, newLen - state.length) 70 | 71 | if let start = position(from: savedRange.start, offset: diff), 72 | let end = position(from: savedRange.end, offset: diff) { 73 | selectedTextRange = textRange(from: start, to: end) 74 | } 75 | } 76 | } 77 | 78 | @objc func textChanged() { 79 | if let formatter = formatter { 80 | let oldState = saveTextFieldState() 81 | super.text = formatter(text) 82 | restoreTextField(to: oldState) 83 | } 84 | } 85 | 86 | } 87 | 88 | #endif 89 | -------------------------------------------------------------------------------- /Pod/Classes/Lifecycle/Framework/DefaultBehaviors.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultBehaviors.swift 3 | // Swiftilities 4 | // 5 | // Created by Michael Skiba on 2/23/17. 6 | // Copyright© 2016 Raizlabs 7 | // 8 | 9 | #if canImport(UIKit) 10 | import Foundation 11 | import ObjectiveC.runtime 12 | import UIKit 13 | 14 | public struct DefaultBehaviors { 15 | 16 | private static let forcedIgnoredClasses: [UIViewController.Type] = [ 17 | LifecycleBehaviorViewController.self, 18 | UINavigationController.self, 19 | UITabBarController.self, 20 | ] 21 | public let behaviors: [ViewControllerLifecycleBehavior] 22 | public let ignoredClasses: [UIViewController.Type] 23 | 24 | public init(behaviors: [ViewControllerLifecycleBehavior], ignoredClasses: [UIViewController.Type] = []) { 25 | self.behaviors = behaviors 26 | self.ignoredClasses = ignoredClasses 27 | } 28 | 29 | /// **Swizzles** `viewDidLoad` to add default behaviors to all view controllers. 30 | /// 31 | /// - Parameter behaviors: The default behaviors to add 32 | public func inject() { 33 | let selector = #selector(UIViewController.viewDidLoad) 34 | typealias ViewDidLoadIMP = @convention(c)(UIViewController, Selector) -> Void 35 | 36 | let instanceViewDidLoad = class_getInstanceMethod(UIViewController.self, selector) 37 | assert(instanceViewDidLoad != nil, "UIViewController should implement \(selector)") 38 | 39 | var originalIMP: IMP? = nil 40 | let ignored = self.ignoredClasses 41 | let behaviors = self.behaviors 42 | let swizzledIMPBlock: @convention(block) (UIViewController) -> Void = { (receiver) in 43 | // Invoke the original IMP if it exists 44 | if originalIMP != nil { 45 | let imp = unsafeBitCast(originalIMP, to: ViewDidLoadIMP.self) 46 | imp(receiver, selector) 47 | } 48 | DefaultBehaviors.inject(behaviors: behaviors, into: receiver, ignoring: ignored) 49 | } 50 | 51 | let swizzledIMP = imp_implementationWithBlock(unsafeBitCast(swizzledIMPBlock, to: AnyObject.self)) 52 | originalIMP = method_setImplementation(instanceViewDidLoad!, swizzledIMP) 53 | 54 | } 55 | 56 | private static func inject(behaviors: [ViewControllerLifecycleBehavior], 57 | into viewController: UIViewController, 58 | ignoring ignoredClasses: [UIViewController.Type]) { 59 | for type in ignoredClasses + forcedIgnoredClasses { 60 | guard !viewController.isKind(of: type) else { 61 | return 62 | } 63 | } 64 | // Prevents swizzing view controllers that are not subclassed from UIKit 65 | let uiKitBundle = Bundle(for: UIViewController.self) 66 | let receiverBundle = Bundle(for: type(of: viewController)) 67 | guard uiKitBundle != receiverBundle else { 68 | return 69 | } 70 | viewController.addBehaviors(behaviors) 71 | } 72 | 73 | } 74 | 75 | #endif 76 | -------------------------------------------------------------------------------- /Pod/Classes/TintedButton/TintedButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TintedButton.swift 3 | // Swiftilities 4 | // 5 | // Created by Michael Skiba on 11/17/16. 6 | // Copyright © 2016 Raizlabs. All rights reserved. 7 | // 8 | 9 | #if canImport(UIKit) 10 | import UIKit 11 | 12 | /// A button that looks and behaves like the default "Get" button in iTunes 13 | /// To fully replacate the effect in views built in interface builder, the button type needs to be set to "custom" 14 | open class TintedButton: UIButton { 15 | 16 | @IBInspectable open var fillColor: UIColor = UIColor.clear { 17 | didSet { 18 | setupBackground() 19 | } 20 | } 21 | 22 | @IBInspectable open var textColor: UIColor? = nil { 23 | didSet { 24 | setupTint() 25 | } 26 | } 27 | 28 | @IBInspectable open var buttonCornerRadius: CGFloat = 4 { 29 | didSet { 30 | setupBorder() 31 | } 32 | } 33 | 34 | @IBInspectable open var buttonBorderWidth: CGFloat = 1 { 35 | didSet { 36 | setupBorder() 37 | } 38 | } 39 | 40 | open override var isHighlighted: Bool { 41 | didSet { 42 | toggleBackground() 43 | } 44 | } 45 | 46 | public init(fillColor: UIColor, textColor: UIColor, buttonCornerRadius: CGFloat = 4.0, buttonBorderWidth: CGFloat = 1.0) { 47 | super.init(frame: CGRect.zero) 48 | self.fillColor = fillColor 49 | self.textColor = textColor 50 | self.buttonCornerRadius = buttonCornerRadius 51 | self.buttonBorderWidth = buttonBorderWidth 52 | setupBackground() 53 | setupTint() 54 | } 55 | 56 | public required init?(coder aDecoder: NSCoder) { 57 | if let color = aDecoder.decodeObject(forKey: #keyPath(fillColor)) as? UIColor { 58 | self.fillColor = color 59 | } 60 | if let color = aDecoder.decodeObject(forKey: #keyPath(tintColor)) as? UIColor { 61 | self.textColor = color 62 | } 63 | super.init(coder: aDecoder) 64 | setupBackground() 65 | setupTint() 66 | } 67 | 68 | open override func encode(with aCoder: NSCoder) { 69 | super.encode(with: aCoder) 70 | aCoder.encode(fillColor, forKey: #keyPath(fillColor)) 71 | aCoder.encode(textColor, forKey: #keyPath(textColor)) 72 | } 73 | 74 | } 75 | 76 | private extension TintedButton { 77 | 78 | func setupBackground() { 79 | setTitleColor(fillColor, for: .highlighted) 80 | toggleBackground() 81 | } 82 | 83 | func toggleBackground() { 84 | if isHighlighted { 85 | backgroundColor = textColor 86 | } 87 | else { 88 | backgroundColor = fillColor 89 | } 90 | } 91 | 92 | func setupTint() { 93 | setTitleColor(textColor, for: .normal) 94 | layer.borderColor = self.textColor?.cgColor 95 | toggleBackground() 96 | setupBorder() 97 | } 98 | 99 | func setupBorder() { 100 | layer.borderWidth = buttonBorderWidth 101 | layer.cornerRadius = buttonCornerRadius 102 | } 103 | } 104 | 105 | #endif 106 | -------------------------------------------------------------------------------- /Example/Tests/MathTests.swift: -------------------------------------------------------------------------------- 1 | import Swiftilities 2 | import XCTest 3 | 4 | class MathTests: XCTestCase { 5 | 6 | let epsilon = 0.00001 7 | 8 | func testFloatingPointScale() { 9 | XCTAssertEqual(1.0.scaled(from: 0.0...1.0, to: 0.0...1.0), 1.0, accuracy: epsilon) 10 | XCTAssertEqual(0.5.scaled(from: 0.0...1.0, to: 0.0...100.0), 50.0, accuracy: epsilon) 11 | 12 | XCTAssertEqual(10.0.scaled(from: 0.0...1.0, to: 0.0...10.0, clamped: false), 100.0, accuracy: epsilon) 13 | XCTAssertEqual(10.0.scaled(from: 0.0...1.0, to: 0.0...10.0, clamped: true), 10.0, accuracy: epsilon) 14 | XCTAssertEqual(10.scaled(from: 0.0...1.0, to: 0.0...10.0, clamped: false), 100.0, accuracy: epsilon) 15 | 16 | XCTAssertEqual(0.25.scaled(from: 0.0...1.0, to: -100.0...100.0), -50.0, accuracy: epsilon) 17 | 18 | // Difference between using and not using parentheses with negation. 19 | 20 | // remap(-10) 21 | XCTAssertEqual((-10.0).scaled(from: -50.0...0.0, to: 0.0...1.0), 0.8, accuracy: epsilon) 22 | 23 | // Negating remap(10) 24 | XCTAssertEqual(-10.0.scaled(from: -50.0...0.0, to: 0.0...1.0), -1.2, accuracy: epsilon) 25 | XCTAssertEqual(-10.0.scaled(from: -50.0...0.0, to: 0.0...1.0, clamped: true), -1.0, accuracy: epsilon) 26 | } 27 | 28 | func testFloatingPointReverseScale() { 29 | XCTAssertEqual(1.0.scaled(from: 0.0...1.0, to: 0.0...1.0, reversed: true), 0.0, accuracy: epsilon) 30 | XCTAssertEqual(0.5.scaled(from: 0.0...1.0, to: 0.0...100.0, reversed: true), 50.0, accuracy: epsilon) 31 | 32 | XCTAssertEqual(10.0.scaled(from: 0.0...1.0, to: 0.0...10.0, clamped: false, reversed: true), -90.0, accuracy: epsilon) 33 | XCTAssertEqual(10.0.scaled(from: 0.0...1.0, to: 0.0...10.0, clamped: true, reversed: true), 0.0, accuracy: epsilon) 34 | XCTAssertEqual(10.scaled(from: 0.0...1.0, to: 0.0...10.0, clamped: false, reversed: true), -90.0, accuracy: epsilon) 35 | 36 | XCTAssertEqual(0.25.scaled(from: 0.0...1.0, to: -100.0...100.0, reversed: true), 50.0, accuracy: epsilon) 37 | 38 | XCTAssertEqual(0.25.scaled(from: 0.0...1.0, to: 0.0...1.0, reversed: true), 0.75, accuracy: epsilon) 39 | 40 | // Difference between using and not using parentheses with negation. 41 | 42 | // remap(-10) 43 | XCTAssertEqual((-10.0).scaled(from: -50.0...0.0, to: 0.0...1.0, reversed: true), 0.2, accuracy: epsilon) 44 | 45 | // Negating remap(10) 46 | XCTAssertEqual(-10.0.scaled(from: -50.0...0.0, to: 0.0...1.0, reversed: true), 0.2, accuracy: epsilon) 47 | XCTAssertEqual(-10.0.scaled(from: -50.0...0.0, to: 0.0...1.0, clamped: true, reversed: true), 0.0, accuracy: epsilon) 48 | } 49 | 50 | func testClamping() { 51 | XCTAssertEqual(0.clamped(to: -10...20), 0) 52 | XCTAssertEqual((-10).clamped(to: -10...20), -10) 53 | XCTAssertEqual(UInt(0).clamped(to: 10...20), 10) 54 | XCTAssertEqual(Double.pi.clamped(to: 0...1), 1, accuracy: epsilon) 55 | XCTAssertEqual(Double.pi.clamped(to: 3...4), Double.pi, accuracy: epsilon) 56 | XCTAssertEqual(-20.clamped(to: 0...1), -1, accuracy: epsilon) 57 | XCTAssertEqual((-20).clamped(to: 0...1), 0, accuracy: epsilon) 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /Pod/Classes/Lifecycle/Framework/LifecycleBehaviorViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LifecycleBehaviorViewController.swift 3 | // Swiftilities 4 | // 5 | // Created by Nicholas Bonatsakis on 6/10/16. 6 | // Copyright© 2016 Raizlabs 7 | // 8 | // Based on concepts and examples in: 9 | // http://khanlou.com/2016/02/many-controllers/ and 10 | // http://irace.me/lifecycle-behaviors 11 | 12 | #if canImport(UIKit) 13 | import UIKit 14 | 15 | final class LifecycleBehaviorViewController: UIViewController { 16 | 17 | fileprivate let behaviors: [ViewControllerLifecycleBehavior] 18 | 19 | // MARK: - Initialization 20 | 21 | init(behaviors: [ViewControllerLifecycleBehavior]) { 22 | self.behaviors = behaviors 23 | 24 | super.init(nibName: nil, bundle: nil) 25 | } 26 | 27 | required init?(coder aDecoder: NSCoder) { 28 | fatalError("init(coder:) has not been implemented") 29 | } 30 | 31 | // MARK: - UIViewController 32 | 33 | override func viewDidLoad() { 34 | super.viewDidLoad() 35 | 36 | view.isHidden = true 37 | } 38 | 39 | override func viewWillAppear(_ animated: Bool) { 40 | super.viewWillAppear(animated) 41 | 42 | applyBehaviors { behavior, viewController in 43 | behavior.beforeAppearing(viewController, animated: animated) 44 | } 45 | } 46 | 47 | override func viewDidAppear(_ animated: Bool) { 48 | super.viewDidAppear(animated) 49 | 50 | applyBehaviors { behavior, viewController in 51 | behavior.afterAppearing(viewController, animated: animated) 52 | } 53 | } 54 | 55 | override func viewWillDisappear(_ animated: Bool) { 56 | super.viewWillDisappear(animated) 57 | 58 | applyBehaviors { behavior, viewController in 59 | behavior.beforeDisappearing(viewController, animated: animated) 60 | } 61 | } 62 | 63 | override func viewDidDisappear(_ animated: Bool) { 64 | super.viewDidDisappear(animated) 65 | 66 | applyBehaviors { behavior, viewController in 67 | behavior.afterDisappearing(viewController, animated: animated) 68 | } 69 | } 70 | 71 | override func viewWillLayoutSubviews() { 72 | super.viewWillLayoutSubviews() 73 | 74 | applyBehaviors { behavior, viewController in 75 | behavior.beforeLayingOutSubviews(viewController) 76 | } 77 | } 78 | 79 | override func viewDidLayoutSubviews() { 80 | super.viewDidLayoutSubviews() 81 | 82 | applyBehaviors { behavior, viewController in 83 | behavior.afterLayingOutSubviews(viewController) 84 | } 85 | } 86 | 87 | } 88 | 89 | private extension LifecycleBehaviorViewController { 90 | 91 | typealias BehaviorApplication = (_ behavior: ViewControllerLifecycleBehavior, 92 | _ viewController: UIViewController) -> Void 93 | 94 | func applyBehaviors(body: BehaviorApplication) { 95 | guard let parentViewController = parent else { return } 96 | 97 | for behavior in behaviors { 98 | body(behavior, parentViewController) 99 | } 100 | } 101 | 102 | } 103 | 104 | #endif 105 | -------------------------------------------------------------------------------- /Pod/Classes/DeviceSize/DeviceSize.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DeviceSize.swift 3 | // Swiftilities 4 | // 5 | // Created by Rob Cadwallader on 11/1/16. 6 | // 7 | // Tools for adjusting values given the current screen size. 8 | // 9 | 10 | #if canImport(UIKit) 11 | import Foundation 12 | import UIKit 13 | 14 | //swiftlint:disable identifier_name 15 | public enum Axis: Int { 16 | case x 17 | case y 18 | } 19 | //swiftlint:enable identifier_name 20 | 21 | /** 22 | Represents the screen size of a device 23 | */ 24 | public enum DeviceSize: Hashable { 25 | 26 | case small // e.g. Original iPhone -> iPhone 4s 27 | case medium // e.g. iPhone 5, iPod Touch 5th and 6th gen 28 | case large // e.g. iPhone 6, iPhone 7 29 | case plus // e.g. iPhone 6 Plus, iPhone 7 Plus 30 | case other(CGSize) 31 | 32 | init(size: CGSize) { 33 | switch size { 34 | case DeviceSize.small.dimensions: 35 | self = .small 36 | case DeviceSize.medium.dimensions: 37 | self = .medium 38 | case DeviceSize.large.dimensions: 39 | self = .large 40 | case DeviceSize.plus.dimensions: 41 | self = .plus 42 | default: 43 | self = .other(size) 44 | } 45 | } 46 | 47 | /** 48 | Screen size of the current device as reported by UIScreen 49 | */ 50 | public static var current: DeviceSize = DeviceSize(size: UIScreen.main.bounds.size) 51 | 52 | /** 53 | Conditionally return a value given the current device screen size. For example: 54 | 55 | let result = DeviceSize.adjust(25, for: [.small: 50, .large: 100]) 56 | print(result) // Prints 100 when DeviceSize.current == .large 57 | 58 | - parameter default: Value that will be returned if DeviceSize.current is not a key in overrides. 59 | - parameter overrides: A dictionary specifying the values to return for specific DeviceSizes. 60 | */ 61 | public static func adjust(_ default: ValueType, for overrides: [DeviceSize: ValueType]) -> ValueType { 62 | return overrides[DeviceSize.current] ?? `default` 63 | } 64 | } 65 | 66 | public extension DeviceSize { 67 | 68 | var dimensions: CGSize { 69 | switch self { 70 | case .small: return CGSize(width: 320, height: 480) 71 | case .medium: return CGSize(width: 320, height: 568) 72 | case .large: return CGSize(width: 375, height: 667) 73 | case .plus: return CGSize(width: 414, height: 736) 74 | case .other(let size): return size 75 | } 76 | } 77 | 78 | 79 | #if swift(>=4.2) 80 | func hash(into hasher: inout Hasher) { 81 | let a: Int = Int(dimensions.width) 82 | let b: Int = Int(dimensions.height) 83 | hasher.combine(a) 84 | hasher.combine(b) 85 | } 86 | #else 87 | var hashValue: Int { 88 | let a: Int = Int(dimensions.width) 89 | let b: Int = Int(dimensions.height) 90 | 91 | // Using Cantor's pairing function: 92 | return (a + b) * (a + b + 1) / 2 + b 93 | } 94 | #endif 95 | } 96 | 97 | public func == (lhs: DeviceSize, rhs: DeviceSize) -> Bool { 98 | return lhs.hashValue == rhs.hashValue 99 | } 100 | 101 | #endif 102 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Swiftilities 2 | 3 | 4 | [![Swift 4.2, 5.1](https://img.shields.io/badge/Swift-4.2,%205.1-orange.svg?style=flat)](https://swift.org) 5 | [![CircleCI](https://img.shields.io/circleci/project/github/Rightpoint/Swiftilities.svg)](https://circleci.com/gh/Rightpoint/Swiftilities/tree/master) 6 | [![Version](https://img.shields.io/cocoapods/v/Swiftilities.svg?style=flat)](https://cocoapods.org/pods/Swiftilities) 7 | [![License](https://img.shields.io/cocoapods/l/Swiftilities.svg?style=flat)](https://cocoapods.org/pods/Swiftilities) 8 | [![Platform](https://img.shields.io/cocoapods/p/Swiftilities.svg?style=flat)](https://cocoapods.org/pods/Swiftilities) 9 | [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) 10 | 11 | ## What's Inside? 12 | 13 | - AboutView 14 | - AccessibilityHelpers 15 | - [Acknowledgements](Pod/Classes/Acknowledgements/README.md) - Simple solution for adding acknowledgement section with pod licenses. 16 | - BetterButton 17 | - ColorHelpers 18 | - Compatibility 19 | - CoreDataStack 20 | - Deselection 21 | - DeviceSize 22 | - FormattedTextField 23 | - Forms 24 | - [HairlineView](Pod/Classes/HairlineView/README.md) - A horizontal or vertical hairline view 25 | - ImageHelpers 26 | - Keyboard 27 | - LicenseFormatter 28 | - [Lifecycle](Pod/Classes/Lifecycle/readme.md) - Declaratively customize your app's behavior and look/feel 29 | - [Logging](Pod/Classes/Logging/README.md) - Log events by priority 30 | - Math 31 | - RootViewController 32 | - Shapes 33 | - StackViewHelpers 34 | - TableViewHelpers 35 | - [TintedButton](Pod/Classes/TintedButton/README.md) - UIButton with border and default highlighting 36 | - Views: 37 | - [GradientView](Pod/Classes/Views/Gradient/README.md) - UIView containing a color gradient. 38 | - [Text Views](Pod/Classes/Views/Textview/README.md) - UITextViews that can present placeholder text or expand height to accommodate content. 39 | 40 | ## Usage 41 | 42 | To run the example project, clone the repo, and run `pod install` from the Example directory first. 43 | 44 | ### Adding A New Subspec 45 | 1. Create a new directory within the Classes folder (or Assets, if required) 46 | 2. Add the new files to the directory created in step 1 47 | 3. Add a subspec to the Swiftilities.podspec following this pattern: 48 | ```ruby 49 | # 50 | 51 | s.subspec "" do |ss| 52 | ss.source_files = "Pod/Classes//*.swift" 53 | ss.frameworks = [""] 54 | end 55 | ``` 56 | 4. Append an `ss.dependency` to `s.subspec` within the podspec file with the following format: 57 | 58 | ```ruby 59 | ss.dependency 'Swiftilities/' 60 | ``` 61 | 62 | 5. Navigate to the example project directory and run `bundle exec pod update` 63 | 64 | ## Requirements 65 | 66 | ## Installation 67 | 68 | Swiftilities is available through [CocoaPods](http://cocoapods.org). To install 69 | it, simply add the following line to your Podfile: 70 | 71 | ```ruby 72 | pod "Swiftilities" 73 | ``` 74 | 75 | ## Author 76 | 77 | Rightpoint, opensource@rightpoint.com 78 | 79 | ## Code of Conduct 80 | Please read our contribution [Code of Conduct](./CONTRIBUTING.md). 81 | 82 | ## License 83 | 84 | Swiftilities is available under the MIT license. See the LICENSE file for more info. 85 | -------------------------------------------------------------------------------- /Pod/Classes/AboutView/AboutView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AboutView.swift 3 | // Swiftilities 4 | // 5 | // Created by Michael Skiba on 2/28/17. 6 | // Copyright © 2017 Raizlabs. All rights reserved. 7 | // 8 | 9 | #if canImport(UIKit) 10 | import UIKit 11 | 12 | open class AboutView: UIView { 13 | 14 | open private(set) var imageView: UIImageView = { 15 | let imageView = UIImageView() 16 | 17 | imageView.accessibilityTraits = UIAccessibilityTraits.none 18 | 19 | imageView.backgroundColor = .clear 20 | imageView.contentMode = .scaleAspectFit 21 | imageView.translatesAutoresizingMaskIntoConstraints = false 22 | 23 | return imageView 24 | }() 25 | 26 | open private(set) var label: UILabel = { 27 | let label = UILabel() 28 | 29 | label.backgroundColor = .clear 30 | label.textAlignment = .center 31 | label.textColor = UIColor(red: 0.5, green: 0.5, blue: 0.51, alpha: 1) 32 | label.font = UIFont.systemFont(ofSize: 15) 33 | label.text = "\(AppInfo.version) (\(AppInfo.buildNumber))" 34 | label.accessibilityLabel = String(format: NSLocalizedString("Version %@ build %@", comment: "Accessibility version of app version and build number label"), AppInfo.accessibleVersion, AppInfo.buildNumber) 35 | label.translatesAutoresizingMaskIntoConstraints = false 36 | 37 | return label 38 | }() 39 | 40 | private var internalConstraints: [NSLayoutConstraint] = [] 41 | 42 | /// Creates a UIView with an image view displaying the supplied image, and a label with version info beneath it 43 | /// 44 | /// This view implements autolayout, to find the correct height for the view, use `systemLayoutSizeFittingSize` 45 | /// 46 | /// - Parameters: 47 | /// - image: The image to display 48 | /// - imageAccessibilityLabel: The text equivalent of the image, for users of VoiceOver 49 | /// and other assistive technologies. 50 | /// - frame: The initial frame of the view 51 | public init(image: UIImage, imageAccessibilityLabel: String? = nil, frame: CGRect = CGRect()) { 52 | super.init(frame: frame) 53 | 54 | imageView.image = image 55 | imageView.accessibilityLabel = imageAccessibilityLabel 56 | imageView.isAccessibilityElement = (imageAccessibilityLabel != nil) 57 | 58 | prepareView() 59 | prepareLayout() 60 | } 61 | 62 | @available(*, unavailable) required public init?(coder aDecoder: NSCoder) { 63 | fatalError("init(coder:) has not been implemented") 64 | } 65 | 66 | } 67 | 68 | private extension AboutView { 69 | 70 | func prepareView() { 71 | addSubview(imageView) 72 | addSubview(label) 73 | } 74 | 75 | func prepareLayout() { 76 | NSLayoutConstraint.activate([ 77 | imageView.topAnchor.constraint(equalTo: topAnchor, constant: 31), 78 | imageView.leadingAnchor.constraint(equalTo: leadingAnchor), 79 | trailingAnchor.constraint(equalTo: imageView.trailingAnchor), 80 | label.topAnchor.constraint(equalTo: imageView.bottomAnchor), 81 | label.leadingAnchor.constraint(equalTo: leadingAnchor), 82 | trailingAnchor.constraint(equalTo: label.trailingAnchor), 83 | bottomAnchor.constraint(equalTo: label.bottomAnchor, constant: 31), 84 | ]) 85 | } 86 | 87 | } 88 | 89 | #endif 90 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at . All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ -------------------------------------------------------------------------------- /Pod/Classes/AccessibilityHelpers/AccessibilityHelpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AccessibilityHelpers.swift 3 | // Swiftilities 4 | // 5 | // Created by Michael Skiba on 11/17/16. 6 | // Copyright © 2016 Raizlabs. All rights reserved. 7 | // 8 | 9 | #if canImport(UIKit) 10 | import UIKit 11 | 12 | public typealias AccessibilityAnnounceCompletion = (_ anncouncedString: String?, _ success: Bool) -> Void 13 | 14 | /// A set of handy UIAccessibility helpers 15 | public class Accessibility: NSObject { 16 | 17 | public static let shared = Accessibility() 18 | 19 | private var announceCompletion: AccessibilityAnnounceCompletion? 20 | private let concurrentAnnouncementQueue = DispatchQueue(label: "com.raizlabs.announcements.queue") 21 | 22 | public override init() { 23 | super.init() 24 | NotificationCenter.default.addObserver(self, 25 | selector: #selector(Accessibility.handleAnnounceDidFinish(_:)), 26 | name: UIAccessibility.announcementDidFinishNotification, 27 | object: nil) 28 | } 29 | 30 | /** 31 | Check if VoiceOver is currently running (UIAccessibilityIsVoiceOverRunning()). 32 | */ 33 | public static var isVoiceOverRunning: Bool { 34 | return UIAccessibility.isVoiceOverRunning 35 | } 36 | 37 | /** 38 | Announce a message via VoiceOver with optional completion callback (UIAccessibilityAnnouncementNotification). 39 | 40 | - parameter text: The text to be spoken. 41 | - parameter completion: A block to be invoked when the announcement has completed. 42 | */ 43 | public func announce(_ text: String, completion: AccessibilityAnnounceCompletion? = nil) { 44 | concurrentAnnouncementQueue.sync { 45 | announceCompletion?(nil, false) 46 | announceCompletion = completion 47 | } 48 | UIAccessibility.post(notification: UIAccessibility.Notification.announcement, argument: text) 49 | } 50 | 51 | @objc public func handleAnnounceDidFinish(_ notification: NSNotification) { 52 | if let userInfo = notification.userInfo { 53 | concurrentAnnouncementQueue.sync { 54 | announceCompletion?(userInfo[UIAccessibility.announcementStringValueUserInfoKey] as? String, 55 | (userInfo[UIAccessibility.announcementWasSuccessfulUserInfoKey] as? Bool ?? false)) 56 | announceCompletion = nil 57 | } 58 | } 59 | } 60 | 61 | /** 62 | Notify VoiceOver that layout has changed and focus on an optionally provided view (UIAccessibilityLayoutChangedNotification). 63 | 64 | - parameter focusView: A view to be focussed on as part of the layout change. 65 | */ 66 | public func layoutChanged(in focusView: UIView? = nil) { 67 | UIAccessibility.post(notification: UIAccessibility.Notification.layoutChanged, argument: focusView) 68 | } 69 | 70 | } 71 | 72 | // MARK: - UIAccessibility helpers for UITableView 73 | public extension UITableView { 74 | 75 | /** 76 | Focus the VoiceOver layout on the first cell of this UITableView instance. 77 | If the table view has no rows, this is a no-op. 78 | */ 79 | @nonobjc func accessibilityFocusOnFirstCell() { 80 | guard let sections = dataSource?.numberOfSections?(in: self), sections > 0, 81 | let rows = dataSource?.tableView(self, numberOfRowsInSection: 0), rows > 0, 82 | let cell = self.cellForRow(at: IndexPath(row: 0, section: 0)) else { 83 | return 84 | } 85 | Accessibility.shared.layoutChanged(in: cell) 86 | } 87 | 88 | } 89 | 90 | #endif 91 | -------------------------------------------------------------------------------- /Pod/Classes/Keyboard/UIView+KeyboardLayoutGuide.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+KeyboardLayoutGuide.swift 3 | // Swiftilities 4 | // 5 | // Created by Rob Visentin on 2/8/16. 6 | // Copyright © 2016 Raizlabs. All rights reserved. 7 | // 8 | 9 | #if canImport(UIKit) 10 | import UIKit 11 | 12 | /** 13 | * A UIView extension that exposes the keyboard frame as a layout guide (UILayoutGuide). 14 | */ 15 | public extension UIView { 16 | 17 | fileprivate class KeyboardLayoutGuide: UILayoutGuide {} 18 | 19 | /// A layout guide for the keyboard 20 | @nonobjc var keyboardLayoutGuide: UILayoutGuide? { 21 | #if swift(>=5.0) 22 | if let existingIdx = layoutGuides.firstIndex(where: { $0 is KeyboardLayoutGuide }) { 23 | return layoutGuides[existingIdx] 24 | } 25 | #else 26 | if let existingIdx = layoutGuides.index(where: { $0 is KeyboardLayoutGuide }) { 27 | return layoutGuides[existingIdx] 28 | } 29 | #endif 30 | return nil 31 | } 32 | 33 | @nonobjc var transitionCoordinator: UIViewControllerTransitionCoordinator? { 34 | var responder: UIResponder? = next 35 | while responder != nil { 36 | if let coordinator = (responder as? UIViewController)?.transitionCoordinator { 37 | return coordinator 38 | } 39 | responder = responder?.next 40 | } 41 | return nil 42 | } 43 | 44 | /** 45 | Add and configure a keyboard layout guide. 46 | 47 | - returns: A new keyboard layout guide or existing if previously invoked on this view 48 | */ 49 | @nonobjc func addKeyboardLayoutGuide() -> UILayoutGuide { 50 | // Return the existing keyboard layout guide if one has already been added 51 | if let existingGuide = keyboardLayoutGuide { 52 | return existingGuide 53 | } 54 | 55 | let guide = KeyboardLayoutGuide() 56 | addLayoutGuide(guide) 57 | 58 | guide.leftAnchor.constraint(equalTo: leftAnchor).isActive = true 59 | guide.rightAnchor.constraint(equalTo: rightAnchor).isActive = true 60 | guide.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true 61 | 62 | let topConstraint = guide.topAnchor.constraint(equalTo: bottomAnchor) 63 | topConstraint.isActive = true 64 | 65 | Keyboard.addFrameObserver(guide) { [weak self] keyboardFrame in 66 | if let sself = self, sself.window != nil { 67 | var frameInWindow = sself.frame 68 | 69 | if let superview = sself.superview { 70 | frameInWindow = superview.convert(sself.frame, to: nil) 71 | } 72 | 73 | topConstraint.constant = min(0.0, -(frameInWindow.maxY - keyboardFrame.minY)) 74 | 75 | if let coordinator = sself.transitionCoordinator { 76 | coordinator.animate(alongsideTransition: { _ in 77 | UIView.performWithoutAnimation { 78 | sself.layoutIfNeeded() 79 | } 80 | }, completion: nil) 81 | } 82 | else { 83 | sself.layoutIfNeeded() 84 | } 85 | } 86 | } 87 | 88 | return guide 89 | } 90 | 91 | /** 92 | Remove the keyboard layout guide. NOTE: you do not need to invoke this method, it is purely optional. 93 | */ 94 | @nonobjc func removeKeyboardLayoutGuide() { 95 | if let keyboardLayoutGuide = keyboardLayoutGuide { 96 | Keyboard.removeFrameObserver(keyboardLayoutGuide) 97 | removeLayoutGuide(keyboardLayoutGuide) 98 | } 99 | } 100 | 101 | } 102 | 103 | #endif 104 | -------------------------------------------------------------------------------- /Pod/Classes/Acknowledgements/AcknowledgementsListViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AcknowledgementsListViewController.swift 3 | // Swiftilities 4 | // 5 | // Created by Michael Skiba on 11/21/16. 6 | // Copyright © 2016 Raizlabs. All rights reserved. 7 | // 8 | 9 | #if canImport(UIKit) 10 | import UIKit 11 | 12 | open class AcknowledgementsListViewController: UITableViewController { 13 | 14 | fileprivate static let reuseID = "com.raizlabs.acknowledgements.standardCell" 15 | 16 | public var childViewControllerClass: AcknowledgementViewController.Type = AcknowledgementViewController.self 17 | 18 | public enum LocalizedStrings { 19 | public static let acknowledgementsTitle = NSLocalizedString("Acknowledgements", comment: "Default title for the Acknowledgements view controller from Swiftilities") 20 | } 21 | 22 | open var viewModel: AcknowledgementsListViewModel = AcknowledgementsListViewModel(title: "", acknowledgements: []) { 23 | didSet { 24 | tableView.reloadData() 25 | } 26 | } 27 | 28 | open var licenseFormatter: (String) -> NSAttributedString = AcknowledgementViewController.defaultLicenseFormatter 29 | 30 | open var licenseViewBackgroundColor: UIColor? 31 | 32 | open var cellBackgroundColor: UIColor? { 33 | didSet { 34 | tableView.reloadData() 35 | } 36 | } 37 | 38 | public convenience init(title: String = LocalizedStrings.acknowledgementsTitle, 39 | viewModel: AcknowledgementsListViewModel, 40 | licenseFormatter: @escaping (String) -> NSAttributedString = AcknowledgementViewController.defaultLicenseFormatter) { 41 | self.init(style: .plain) 42 | self.navigationItem.title = title 43 | self.viewModel = viewModel 44 | self.licenseFormatter = licenseFormatter 45 | } 46 | 47 | override open func viewDidLoad() { 48 | super.viewDidLoad() 49 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: AcknowledgementsListViewController.reuseID) 50 | tableView.tableFooterView = UIView(frame: .zero) 51 | } 52 | 53 | override open func viewWillAppear(_ animated: Bool) { 54 | super.viewWillAppear(animated) 55 | smoothlyDeselectItems(tableView) 56 | } 57 | 58 | // MARK: - Table view data source 59 | 60 | override open func numberOfSections(in tableView: UITableView) -> Int { 61 | return viewModel.acknowledgements.isEmpty ? 0 : 1 62 | } 63 | 64 | override open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 65 | return viewModel.acknowledgements.count 66 | } 67 | 68 | override open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 69 | let cell = tableView.dequeueReusableCell(withIdentifier: AcknowledgementsListViewController.reuseID, for: indexPath) 70 | cell.accessoryType = .disclosureIndicator 71 | if let backgroundColor = cellBackgroundColor { 72 | cell.backgroundColor = backgroundColor 73 | } 74 | cell.textLabel?.text = viewModel.acknowledgements[indexPath.row].title 75 | return cell 76 | } 77 | 78 | // MARK: Table view delegate 79 | override open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 80 | let entry = viewModel.acknowledgements[indexPath.row] 81 | let acknowledgementVC = childViewControllerClass.init(viewModel: entry, 82 | licenseFormatter: licenseFormatter, 83 | viewBackgroundColor: licenseViewBackgroundColor) 84 | navigationController?.pushViewController(acknowledgementVC, animated: true) 85 | } 86 | 87 | } 88 | 89 | #endif 90 | -------------------------------------------------------------------------------- /Pod/Classes/Math/CubicBezier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CubicBezier.swift 3 | // Swiftilities 4 | // 5 | // Created by Jason Clark on 8/10/17. 6 | // 7 | /* 8 | Derived from IBM's implementation of the CSS function cubic-bezier 9 | 10 | Licensed Materials - Property of IBM 11 | © Copyright IBM Corporation 2015. All Rights Reserved. 12 | This licensed material is licensed under the Apache 2.0 license. http://www.apache.org/licenses/LICENSE-2.0. 13 | */ 14 | 15 | import CoreGraphics.CGGeometry 16 | 17 | // swiftlint:disable identifier_name 18 | extension CubicBezier { 19 | 20 | static func value(for input: CGFloat, controlPoint1: CGPoint, controlPoint2: CGPoint) -> CGFloat { 21 | return CubicBezier(p1: controlPoint1, p2: controlPoint2).valueForX(x: input) 22 | } 23 | 24 | } 25 | 26 | struct CubicBezier { 27 | 28 | let cx: CGFloat, bx: CGFloat, ax: CGFloat, cy: CGFloat, by: CGFloat, ay: CGFloat 29 | 30 | init(p1: CGPoint, p2: CGPoint) { 31 | self.init(x1: p1.x, y1: p1.y, x2: p2.x, y2: p2.y) 32 | } 33 | 34 | init(x1: CGFloat, y1: CGFloat, x2: CGFloat, y2: CGFloat) { 35 | 36 | var p1 = CGPoint.zero 37 | var p2 = CGPoint.zero 38 | 39 | // Clamp to interval [0..1] 40 | p1.x = max(0.0, min(1.0, x1)) 41 | p1.y = max(0.0, min(1.0, y1)) 42 | 43 | p2.x = max(0.0, min(1.0, x2)) 44 | p2.y = max(0.0, min(1.0, y2)) 45 | 46 | // Implicit first and last control points are (0,0) and (1,1). 47 | cx = 3.0 * p1.x 48 | bx = 3.0 * (p2.x - p1.x) - cx 49 | ax = 1.0 - cx - bx 50 | 51 | cy = 3.0 * p1.y 52 | by = 3.0 * (p2.y - p1.y) - cy 53 | ay = 1.0 - cy - by 54 | } 55 | 56 | func valueForX(x: CGFloat) -> CGFloat { 57 | let epsilon: CGFloat = 1.0 / 200.0 58 | let xSolved = solveCurveX(x, epsilon: epsilon) 59 | let y = sampleCurveY(xSolved) 60 | return (y / epsilon).rounded(.toNearestOrAwayFromZero) * epsilon 61 | } 62 | 63 | private func solveCurveX(_ x: CGFloat, epsilon: CGFloat) -> CGFloat { 64 | 65 | var t0: CGFloat, t1: CGFloat, t2: CGFloat, x2: CGFloat, d2: CGFloat 66 | 67 | // First try a few iterations of Newton's method -- normally very fast. 68 | t2 = x 69 | for _ in 0...8 { 70 | x2 = sampleCurveX(t2) - x 71 | if abs(x2) < epsilon { 72 | return t2 73 | } 74 | d2 = sampleCurveDerivativeX(t2) 75 | if abs(d2) < 1e-6 { 76 | break 77 | } 78 | t2 -= x2 / d2 79 | } 80 | 81 | // Fall back to the bisection method for reliability. 82 | t0 = 0.0 83 | t1 = 1.0 84 | t2 = x 85 | 86 | if t2 < t0 { 87 | return t0 88 | } 89 | if t2 > t1 { 90 | return t1 91 | } 92 | 93 | while t0 < t1 { 94 | x2 = sampleCurveX(t2) 95 | if abs(x2 - x) < epsilon { 96 | return t2 97 | } 98 | if x > x2 { 99 | t0 = t2 100 | } 101 | else { 102 | t1 = t2 103 | } 104 | t2 = (t1 - t0) * 0.5 + t0 105 | } 106 | 107 | // Failure. 108 | return t2 109 | } 110 | 111 | private func sampleCurveX(_ t: CGFloat) -> CGFloat { 112 | // 'ax t^3 + bx t^2 + cx t' expanded using Horner's rule. 113 | return ((ax * t + bx) * t + cx) * t 114 | } 115 | 116 | private func sampleCurveY(_ t: CGFloat) -> CGFloat { 117 | return ((ay * t + by) * t + cy) * t 118 | } 119 | 120 | private func sampleCurveDerivativeX(_ t: CGFloat) -> CGFloat { 121 | return (3.0 * ax * t + 2.0 * bx) * t + cx 122 | } 123 | 124 | } 125 | 126 | -------------------------------------------------------------------------------- /Example/Swiftilities/Assets/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 24 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /Pod/Classes/Acknowledgements/AcknowledgementsListViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AcknowledgementsListViewModel.swift 3 | // Swiftilities 4 | // 5 | // Created by Michael Skiba on 11/21/16. 6 | // Copyright © 2016 Raizlabs. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | private struct AcknowledgmentConstants { 12 | static let settingsKeyTitle = "Title" 13 | static let settingsKeySpecifiers = "PreferenceSpecifiers" 14 | static let settingsKeyFooterText = "FooterText" 15 | } 16 | 17 | public struct AcknowledgementsListViewModel { 18 | 19 | public enum AcknowledgementsError: Error { 20 | case missingPlistNamed(String) 21 | case invalidPlistAtURL(URL) 22 | case noTitle 23 | case noSpecifiers 24 | } 25 | 26 | public var title: String 27 | public var acknowledgements: [AcknowledgementViewModel] 28 | 29 | } 30 | 31 | public extension AcknowledgementsListViewModel { 32 | 33 | init(plistNamed plistName: String = "Acknowledgements", in bundle: Bundle = Bundle.main) throws { 34 | guard let url = bundle.url(forResource: plistName, withExtension: "plist") else { 35 | throw AcknowledgementsError.missingPlistNamed(plistName) 36 | } 37 | let dictionary = try AcknowledgementsListViewModel.loadPlist(at: url) 38 | title = try AcknowledgementsListViewModel.parseTitle(from: dictionary) 39 | acknowledgements = try AcknowledgementsListViewModel.parseAcknowledgements(from: dictionary) 40 | } 41 | 42 | init(plistURL: URL) throws { 43 | let dictionary = try AcknowledgementsListViewModel.loadPlist(at: plistURL) 44 | title = try AcknowledgementsListViewModel.parseTitle(from: dictionary) 45 | acknowledgements = try AcknowledgementsListViewModel.parseAcknowledgements(from: dictionary) 46 | } 47 | 48 | } 49 | 50 | private extension AcknowledgementsListViewModel { 51 | 52 | static func loadPlist(at url: URL) throws -> [String: Any] { 53 | guard let dictionary = NSDictionary(contentsOf: url) as? [String: Any] else { 54 | throw AcknowledgementsError.invalidPlistAtURL(url) 55 | } 56 | return dictionary 57 | } 58 | 59 | static func parseTitle(from dictionary: [String: Any]) throws -> String { 60 | guard let title = dictionary[AcknowledgmentConstants.settingsKeyTitle] as? String else { 61 | throw AcknowledgementsError.noTitle 62 | } 63 | return title 64 | } 65 | 66 | static func parseAcknowledgements(from dictionary: [String: Any]) throws -> [AcknowledgementViewModel] { 67 | guard let specifiers = dictionary[AcknowledgmentConstants.settingsKeySpecifiers] as? [[String: Any]] else { 68 | throw AcknowledgementsError.noSpecifiers 69 | } 70 | guard specifiers.count > 2 else { 71 | return [] 72 | } 73 | // First and last elements in the settings bundle are not needed (title and empty row). 74 | // these are broken up to speed up compile times 75 | let innerRange = 1..<(specifiers.count - 1) 76 | let rawAcknowledgements = specifiers[innerRange].compactMap(AcknowledgementViewModel.init(dictionary:)) 77 | let acknowledgements = rawAcknowledgements.sorted { 78 | return $0.title.localizedCaseInsensitiveCompare($1.title) == .orderedAscending 79 | } 80 | 81 | return acknowledgements 82 | } 83 | 84 | } 85 | 86 | public struct AcknowledgementViewModel { 87 | public let title: String 88 | public let license: String 89 | } 90 | 91 | private extension AcknowledgementViewModel { 92 | 93 | init?(dictionary: [String: Any]) { 94 | guard let title = dictionary[AcknowledgmentConstants.settingsKeyTitle] as? String, 95 | let footerText = dictionary[AcknowledgmentConstants.settingsKeyFooterText] as? String else { 96 | return nil 97 | } 98 | self.title = title 99 | self.license = footerText.cleanedUpLicense 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /Pod/Classes/Acknowledgements/README.md: -------------------------------------------------------------------------------- 1 | # Acknowledgements 2 | 3 | A simple solution for adding an acknowledgement section to your app. Includes all pod licenses from the [Cocoapods generated plist](https://github.com/CocoaPods/CocoaPods/wiki/Acknowledgements). 4 | 5 | ## Default Appearance 6 | 7 |
8 | Screenshots 9 | 10 |

11 | 12 | 13 |

14 | 15 |
16 | 17 | ### Quick Start 18 | 19 | To use `AcknowledgementsListViewController` as-is, follow these 4 easy steps: 20 | 21 | 1. Add `post_install` hook to the end of your `Podfile` to copy the generated `plist` into your project root folder. _Rememeber to replace `` with your project's name_ 22 | ```ruby 23 | post_install do | installer | 24 | require 'fileutils' 25 | FileUtils.cp_r('Pods/Target Support Files/Pods-/Pods-.plist', '/Acknowledgements.plist', :remove_destination => true) 26 | end 27 | ``` 28 | 29 | 2. Create an instance of `AcknowledgementsListViewModel`. Requires a `try` to catch any `throw`. 30 | 31 | 3. Create an instance of `AcknowledgementsListViewController` using your view model. 32 | 33 | 4. Push your view controller onto your existing `UINavigationController` to take advantage of the built-in back button. 34 | ```swift 35 | do { 36 | let viewModel = try AcknowledgementsListViewModel() // STEP 2 37 | let viewController = AcknowledgementsListViewController(viewModel: viewModel) // STEP 3 38 | navigationController?.pushViewController(viewController, animated: true) // STEP 4 39 | catch { 40 | print(error.localizedDescription) 41 | } 42 | ``` 43 | 44 | ## Custom Appearance 45 | 46 |
47 | Screenshots 48 | 49 |

50 | 51 | 52 |

53 | 54 |
55 | 56 | ### AcknowledgementsListViewController 57 | 58 | To customize, just subclass it. Please note that it is already subclassing `UITableViewController` so you may need to override table view methods to further customize the look and feel. 59 | 60 | | Property | Type | Notes | 61 | |----------|------|-------| 62 | | childViewControllerClass | AcknowledgementViewController.Type | Be sure to set this if you are using a custom `AcknowledgementViewController` for the license view. | 63 | | viewModel | AcknowledgementsListViewModel | Best to use as-is. | 64 | | licenseFormatter | (String) -> NSAttributedString | Closure for formatting the text | 65 | | licenseViewBackgroundColor | UIColor | Set the background color for the license view. | 66 | | cellBackgroundColor | UIColor | Set the background color for the list view. | 67 | 68 | ```swift 69 | class CustomAcknowledgementsListViewController: AcknowledgementsListViewController { 70 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 71 | let cell = super.tableView(tableView, cellForRowAt: indexPath) 72 | cell.textLabel?.textColor = .random 73 | return cell 74 | } 75 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 76 | defer { super.tableView(tableView, didSelectRowAt: indexPath) } 77 | guard let cell = tableView.cellForRow(at: indexPath) else { return } 78 | licenseViewBackgroundColor = .random 79 | } 80 | } 81 | ``` 82 | 83 | ```swift 84 | do { 85 | let viewModel = try AcknowledgementsListViewModel() 86 | let viewController = CustomAcknowledgementsListViewController(viewModel: viewModel) 87 | navigationController?.pushViewController(viewController, animated: true) 88 | catch { 89 | print(error.localizedDescription) 90 | } 91 | ``` 92 | 93 | ### AcknowledgementViewController 94 | 95 | There isn't that much benefit to subclassing this. Just use the parent view controller properties `licenseFormatter` and `licenseViewBackgroundColor` instead. 96 | 97 | _Screenshots courtesy of [AcknowledgementSample](https://github.com/pauluhn/AcknowledgementSample)_ 98 | -------------------------------------------------------------------------------- /Example/Pods/Pods.xcodeproj/xcshareddata/xcschemes/Swiftilities.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 49 | 50 | 51 | 52 | 53 | 54 | 64 | 65 | 71 | 72 | 73 | 74 | 75 | 76 | 82 | 83 | 89 | 90 | 91 | 92 | 94 | 95 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /Pod/Classes/Logging/Log.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Log.swift 3 | // Swiftilities 4 | // 5 | // Created by Nicholas Bonatsakis on 2/5/16. 6 | // Copyright © 2016 Raizlabs. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /** 12 | * A simple log that outputs to the console via ```print()```` 13 | */ 14 | open class Log { 15 | 16 | // MARK: Configuration 17 | 18 | /** 19 | Represents a level of detail to be logged. 20 | */ 21 | public enum Level: Int { 22 | case verbose 23 | case debug 24 | case info 25 | case warn 26 | case error 27 | case off 28 | 29 | var name: String { 30 | switch self { 31 | case .verbose: return "Verbose" 32 | case .debug: return "Debug" 33 | case .info: return "Info" 34 | case .warn: return "Warn" 35 | case .error: return "Error" 36 | case .off: return "Disabled" 37 | } 38 | } 39 | 40 | var emoji: String { 41 | switch self { 42 | case .verbose: return "📖" 43 | case .debug: return "🐝" 44 | case .info: return "✏️" 45 | case .warn: return "⚠️" 46 | case .error: return "⁉️" 47 | case .off: return "" 48 | } 49 | } 50 | } 51 | 52 | /// The log level, defaults to .Off 53 | public static var logLevel: Level = .off 54 | 55 | /// If true, prints emojis to signify log type, defaults to off 56 | public static var useEmoji: Bool = false 57 | 58 | /// If this is non-nil, we will call it with the same string that we 59 | /// are going to print to the console. You can use this to pass log 60 | /// messages along to your crash reporter, analytics service, etc. 61 | /// - warning: Be mindful of private user data that might end up in 62 | /// your log statements! Use log levels appropriately 63 | /// to keep private data out of logs that are sent over 64 | /// the Internet. 65 | public static var handler: ((Level, String) -> Void)? 66 | 67 | // MARK: Private 68 | 69 | /// Date formatter for log 70 | fileprivate static let dateformatter: DateFormatter = { 71 | let df = DateFormatter() 72 | df.dateFormat = "Y-MM-dd H:m:ss.SSSS" 73 | return df 74 | }() 75 | 76 | /// Generic log method 77 | fileprivate static func log(_ object: @autoclosure () -> T, level: Log.Level, _ fileName: String, _ functionName: String, _ line: Int) { 78 | if logLevel.rawValue <= level.rawValue { 79 | let date = Log.dateformatter.string(from: Date()) 80 | let components: [String] = fileName.components(separatedBy: "/") 81 | let objectName = components.last ?? "Unknown Object" 82 | let levelString = Log.useEmoji ? level.emoji : "|" + level.name.uppercased() + "|" 83 | let logString = "\(levelString)\(date) \(objectName) \(functionName) line \(line):\n\(object())" 84 | print(logString + "\n") 85 | handler?(level, logString) 86 | } 87 | } 88 | 89 | // MARK: Log Methods 90 | 91 | public static func error(_ object: @autoclosure () -> T, _ fileName: String = #file, _ functionName: String = #function, _ line: Int = #line) { 92 | log(object(), level:.error, fileName, functionName, line) 93 | } 94 | 95 | public static func warn(_ object: @autoclosure () -> T, _ fileName: String = #file, _ functionName: String = #function, _ line: Int = #line) { 96 | log(object(), level:.warn, fileName, functionName, line) 97 | } 98 | 99 | public static func info(_ object: @autoclosure () -> T, _ fileName: String = #file, _ functionName: String = #function, _ line: Int = #line) { 100 | log(object(), level:.info, fileName, functionName, line) 101 | } 102 | 103 | public static func debug(_ object: @autoclosure () -> T, _ fileName: String = #file, _ functionName: String = #function, _ line: Int = #line) { 104 | log(object(), level:.debug, fileName, functionName, line) 105 | } 106 | 107 | public static func verbose(_ object: @autoclosure () -> T, _ fileName: String = #file, _ functionName: String = #function, _ line: Int = #line) { 108 | log(object(), level:.verbose, fileName, functionName, line) 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /Pod/Classes/Acknowledgements/AcknowledgementViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AcknowledgementViewController.swift 3 | // Swiftilities 4 | // 5 | // Created by Michael Skiba on 11/21/16. 6 | // Copyright © 2016 Raizlabs. All rights reserved. 7 | // 8 | 9 | #if canImport(UIKit) 10 | import UIKit 11 | 12 | open class AcknowledgementViewController: UIViewController { 13 | 14 | public static let defaultLicenseFormatter: (String) -> NSAttributedString = { string in 15 | let font = UIFont.preferredFont(forTextStyle: .body) 16 | let paragraphStyle = NSMutableParagraphStyle() 17 | paragraphStyle.hyphenationFactor = 1 18 | paragraphStyle.paragraphSpacing = font.pointSize * 0.5 19 | let attributes: [NSAttributedString.Key: Any] = [ 20 | .font: font, 21 | .paragraphStyle: paragraphStyle, 22 | ] 23 | return NSAttributedString(string: string, attributes: attributes) 24 | } 25 | 26 | public var viewModel = AcknowledgementViewModel(title: "", license: "") { 27 | didSet { 28 | updateWithViewModel() 29 | } 30 | } 31 | 32 | public var scrollView: UIScrollView! { 33 | return view as? UIScrollView 34 | } 35 | 36 | public var licenseFormatter: (String) -> NSAttributedString = defaultLicenseFormatter 37 | 38 | public let labelView: UILabel = { 39 | let labelView = UILabel() 40 | labelView.numberOfLines = 0 41 | return labelView 42 | }() 43 | 44 | public required init(viewModel: AcknowledgementViewModel, licenseFormatter: @escaping (String) -> NSAttributedString, viewBackgroundColor: UIColor?) { 45 | super.init(nibName: nil, bundle: nil) 46 | self.licenseFormatter = licenseFormatter 47 | self.viewModel = viewModel 48 | if let backgroundColor = viewBackgroundColor { 49 | view.backgroundColor = backgroundColor 50 | } 51 | } 52 | 53 | public required init?(coder aDecoder: NSCoder) { 54 | fatalError("init(coder:) has not been implemented") 55 | } 56 | 57 | open override func loadView() { 58 | let scrollView = VerticalScrollView() 59 | scrollView.alwaysBounceVertical = true 60 | view = scrollView 61 | view.backgroundColor = .white 62 | view.addSubview(labelView) 63 | configureLayout() 64 | } 65 | 66 | open override func viewDidLoad() { 67 | super.viewDidLoad() 68 | updateWithViewModel() 69 | } 70 | 71 | } 72 | 73 | private extension AcknowledgementViewController { 74 | 75 | enum LayoutConstants { 76 | static let defaultInset = UIEdgeInsets(top: 14, left: 14, bottom: 14, right: 14) 77 | } 78 | 79 | func configureLayout() { 80 | scrollView.contentInset = LayoutConstants.defaultInset 81 | labelView.translatesAutoresizingMaskIntoConstraints = false 82 | NSLayoutConstraint.activate([ 83 | labelView.topAnchor.constraint(equalTo: view.topAnchor), 84 | labelView.bottomAnchor.constraint(equalTo: view.bottomAnchor), 85 | labelView.leadingAnchor.constraint(equalTo: view.leadingAnchor), 86 | labelView.trailingAnchor.constraint(equalTo: view.trailingAnchor), 87 | ]) 88 | } 89 | 90 | func updateWithViewModel() { 91 | navigationItem.title = viewModel.title 92 | labelView.attributedText = licenseFormatter(viewModel.license) 93 | } 94 | } 95 | 96 | private class VerticalScrollView: UIScrollView { 97 | 98 | private var subviewWidthConstraints: [NSLayoutConstraint] = [] 99 | 100 | override var contentInset: UIEdgeInsets { 101 | didSet { 102 | updateContentInset() 103 | } 104 | } 105 | 106 | override func didAddSubview(_ subview: UIView) { 107 | super.didAddSubview(subview) 108 | updateContentInset() 109 | } 110 | 111 | func updateContentInset() { 112 | NSLayoutConstraint.deactivate(subviewWidthConstraints) 113 | let newConstraints: [NSLayoutConstraint] = subviews.map { subView in 114 | let constriant = subView.widthAnchor.constraint(equalTo: widthAnchor, 115 | constant: -(contentInset.left + contentInset.right)) 116 | constriant.priority = UILayoutPriority.defaultHigh 117 | return constriant 118 | } 119 | subviewWidthConstraints = newConstraints 120 | NSLayoutConstraint.activate(newConstraints) 121 | } 122 | } 123 | 124 | #endif 125 | -------------------------------------------------------------------------------- /Pod/Classes/HairlineView/HairlineView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HairlineView.swift 3 | // Swiftilities 4 | // 5 | // Created by Michael Skiba on 11/17/16. 6 | // Copyright © 2016 Raizlabs. All rights reserved. 7 | // 8 | 9 | #if canImport(UIKit) 10 | import UIKit 11 | 12 | open class HairlineView: UIView { 13 | 14 | #if TARGET_INTERFACE_BUILDER 15 | @IBInspectable open var axis: Int = 0 16 | #else 17 | @objc open var axis: NSLayoutConstraint.Axis = .horizontal { 18 | didSet { 19 | invalidateIntrinsicContentSize() 20 | setNeedsUpdateConstraints() 21 | } 22 | } 23 | #endif 24 | 25 | @IBInspectable open var thickness: CGFloat = CGFloat(1.0 / UIScreen.main.scale) { 26 | didSet { 27 | invalidateIntrinsicContentSize() 28 | setNeedsUpdateConstraints() 29 | } 30 | } 31 | 32 | @IBInspectable open var hairlineColor: UIColor = UIColor.darkGray { 33 | willSet { 34 | update(hairlineColor: newValue) 35 | } 36 | didSet { 37 | setNeedsDisplay() 38 | } 39 | } 40 | 41 | public init(axis: NSLayoutConstraint.Axis = .horizontal, thickness: CGFloat = CGFloat(1.0 / UIScreen.main.scale), 42 | hairlineColor: UIColor = UIColor.darkGray) { 43 | self.axis = axis 44 | self.thickness = thickness 45 | self.hairlineColor = hairlineColor 46 | super.init(frame: .zero) 47 | update(hairlineColor: hairlineColor) 48 | contentMode = .redraw 49 | 50 | setNeedsUpdateConstraints() 51 | } 52 | 53 | public required init?(coder aDecoder: NSCoder) { 54 | if aDecoder.containsValue(forKey: #keyPath(axis)) { 55 | guard let decodedAxis = NSLayoutConstraint.Axis(rawValue: aDecoder.decodeInteger(forKey: #keyPath(axis))) else { 56 | return nil 57 | } 58 | axis = decodedAxis 59 | } 60 | if aDecoder.containsValue(forKey: #keyPath(thickness)) { 61 | thickness = CGFloat(aDecoder.decodeFloat(forKey: #keyPath(thickness))) 62 | } 63 | if aDecoder.containsValue(forKey: #keyPath(hairlineColor)) { 64 | guard let decodedHairline = aDecoder.decodeObject(forKey: #keyPath(hairlineColor)) as? UIColor else { 65 | return nil 66 | } 67 | hairlineColor = decodedHairline 68 | } 69 | super.init(coder: aDecoder) 70 | setNeedsUpdateConstraints() 71 | } 72 | 73 | open override func draw(_ rect: CGRect) { 74 | super.draw(rect) 75 | hairlineColor.setFill() 76 | UIRectFill(rect) 77 | } 78 | 79 | open override func updateConstraints() { 80 | defer { 81 | super.updateConstraints() 82 | } 83 | autoresizingMask.insert([.flexibleHeight, .flexibleWidth]) 84 | } 85 | 86 | open override func encode(with aCoder: NSCoder) { 87 | super.encode(with: aCoder) 88 | aCoder.encode(axis.rawValue, forKey: #keyPath(axis)) 89 | aCoder.encode(thickness, forKey: #keyPath(thickness)) 90 | aCoder.encode(hairlineColor, forKey: #keyPath(hairlineColor)) 91 | } 92 | 93 | open override var intrinsicContentSize: CGSize { 94 | var size = CGSize(width: UIView.noIntrinsicMetric, height: UIView.noIntrinsicMetric) 95 | 96 | switch axis { 97 | case .horizontal: size.height = thickness 98 | case .vertical: size.width = thickness 99 | #if swift(>=5.0) 100 | @unknown default: 101 | debugPrint("ERROR: Unhandled NSLayoutConstraint.Axis case \(axis)!") 102 | break 103 | #endif 104 | } 105 | 106 | return size 107 | } 108 | 109 | open override func contentHuggingPriority(for axis: NSLayoutConstraint.Axis) -> UILayoutPriority { 110 | return (self.axis == axis ? UILayoutPriority.required : UILayoutPriority.defaultLow) 111 | } 112 | 113 | open override func contentCompressionResistancePriority(for axis: NSLayoutConstraint.Axis) -> UILayoutPriority { 114 | return contentHuggingPriority(for: axis) 115 | } 116 | 117 | private func update(hairlineColor: UIColor) { 118 | var red: CGFloat = 0, green: CGFloat = 0, blue: CGFloat = 0, alpha: CGFloat = 0 119 | hairlineColor.getRed(&red, green: &green, blue: &blue, alpha: &alpha) 120 | if alpha != 1.0 { 121 | self.alpha = alpha 122 | let solid = hairlineColor.withAlphaComponent(1.0) 123 | self.hairlineColor = solid 124 | } 125 | } 126 | 127 | } 128 | 129 | #endif 130 | -------------------------------------------------------------------------------- /Pod/Classes/Views/Gradient/GradientView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GradientView.swift 3 | // Swiftilities 4 | // 5 | // Created by Nick Bonatsakis on 5/1/17. 6 | // Copyright © 2017 Raizlabs. All rights reserved. 7 | // 8 | 9 | #if canImport(UIKit) 10 | import UIKit 11 | 12 | /// A basic view that wraps CAGradientLayer 13 | public class GradientView: UIView { 14 | 15 | private struct Constants { 16 | static let leftToRightStart = CGPoint(x: 0, y: 0.5) 17 | static let leftToRightEnd = CGPoint(x: 1, y: 0.5) 18 | static let topToBottomStart = CGPoint(x: 0.5, y: 0) 19 | static let topToBottomEnd = CGPoint(x: 0.5, y: 1) 20 | } 21 | 22 | /// The direction of the gradient 23 | /// 24 | /// - leftToRight: Horizontal direction. 25 | /// - topToBottom: Vertical direction. 26 | /// - custom: Provide start and end CGPoint to specify a custom direction. 27 | public enum Direction { 28 | case leftToRight 29 | case topToBottom 30 | case custom(start: CGPoint, end: CGPoint) 31 | 32 | init(gradientLayer: CAGradientLayer) { 33 | switch (gradientLayer.startPoint, gradientLayer.endPoint) { 34 | case (Constants.leftToRightStart, Constants.leftToRightEnd): self = .leftToRight 35 | case (Constants.topToBottomStart, Constants.topToBottomEnd): self = .topToBottom 36 | default: self = .custom(start: gradientLayer.startPoint, end: gradientLayer.endPoint) 37 | } 38 | } 39 | 40 | var start: CGPoint { 41 | switch self { 42 | case .leftToRight: return Constants.leftToRightStart 43 | case .topToBottom: return Constants.topToBottomStart 44 | case .custom(let start, _): return start 45 | } 46 | } 47 | 48 | var end: CGPoint { 49 | switch self { 50 | case .leftToRight: return Constants.leftToRightEnd 51 | case .topToBottom: return Constants.topToBottomEnd 52 | case .custom(_, let end): return end 53 | } 54 | } 55 | } 56 | 57 | override public class var layerClass: AnyClass { 58 | return CAGradientLayer.self 59 | } 60 | 61 | /// The underlying CAGradientLayer. Accessing this property directly can be useful 62 | /// for situations that aren't covered via this wrapper class, in particular animating 63 | /// any changes to the gradient. 64 | public var gradientLayer: CAGradientLayer { 65 | guard let layer = self.layer as? CAGradientLayer else { 66 | fatalError("Backing layer must be a CAGradientLayer") 67 | } 68 | return layer 69 | } 70 | 71 | /// Passthrough to the underlying `CAGradientLayer` `colors`. 72 | var colors: [UIColor] { 73 | get { 74 | guard let colors = gradientLayer.colors as? [CGColor] else { 75 | return [] 76 | } 77 | return colors.map({ UIColor(cgColor: $0) }) 78 | } 79 | set { 80 | gradientLayer.colors = newValue.map({ $0.cgColor }) 81 | } 82 | } 83 | 84 | /// Passthrough to the underlying `CAGradientLayer` `locations`. 85 | var locations: [Double]? { 86 | get { 87 | return gradientLayer.locations as? [Double] 88 | } 89 | set { 90 | gradientLayer.locations = newValue as [NSNumber]? 91 | } 92 | } 93 | 94 | /// Passthrough to underlying `CAGradientLayer` `startPoint` and `endPoint`. 95 | var direction: Direction { 96 | get { 97 | return Direction(gradientLayer: gradientLayer) 98 | } 99 | set { 100 | gradientLayer.startPoint = newValue.start 101 | gradientLayer.endPoint = newValue.end 102 | } 103 | } 104 | 105 | /// Create a new instance. 106 | /// 107 | /// - Parameters: 108 | /// - direction: The direction in which the gradient will be rendered. 109 | /// - colors: An array of colors to be included in the gradient. 110 | /// - locations: An optional list of gradient stops. If none are provided, the default behavior is to arrange the colors evenly. 111 | public init(direction: Direction, colors: [UIColor], locations: [Double]? = nil) { 112 | super.init(frame: .zero) 113 | self.direction = direction 114 | self.colors = colors 115 | self.locations = locations 116 | } 117 | 118 | required public init?(coder aDecoder: NSCoder) { 119 | super.init(coder: aDecoder) 120 | self.direction = .leftToRight 121 | self.colors = [.white, .black] 122 | } 123 | 124 | } 125 | 126 | #endif 127 | -------------------------------------------------------------------------------- /Example/Tests/TableViewHelperTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableViewHelperTests.swift 3 | // Swiftilities 4 | // 5 | // Created by Zev Eisenberg on 4/19/17. 6 | // Copyright © 2017 Raizlabs. All rights reserved. 7 | // 8 | 9 | #if canImport(UIKit) 10 | import UIKit 11 | import Swiftilities 12 | import XCTest 13 | 14 | enum Row { 15 | 16 | case title 17 | case body 18 | case image 19 | 20 | } 21 | 22 | final class TableViewTester: NSObject { 23 | 24 | let tableView = UITableView() 25 | 26 | let sections: [TableSection] 27 | 28 | init(sections: [TableSection]) { 29 | self.sections = sections 30 | super.init() 31 | tableView.dataSource = self 32 | } 33 | 34 | } 35 | 36 | extension TableViewTester: UITableViewDataSource { 37 | 38 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 39 | return UITableViewCell() 40 | } 41 | 42 | func numberOfSections(in tableView: UITableView) -> Int { 43 | return sections.count 44 | } 45 | 46 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 47 | return sections[section].rows.count 48 | } 49 | 50 | } 51 | 52 | class TableViewHelperTests: XCTestCase { 53 | 54 | let sections = [ 55 | TableSection(section: "Section 0", rows: [ 56 | Row.title, 57 | .image, 58 | .body, 59 | .image, 60 | .body, 61 | .body, 62 | ]), 63 | TableSection(section: "Section 1", rows: [ 64 | .image, 65 | .image, 66 | .image, 67 | ]), 68 | TableSection(section: "Section 2", rows: [ 69 | .title, 70 | ]), 71 | ] 72 | 73 | func testTableSection() { 74 | XCTAssertEqual(sections[0].section, "Section 0") 75 | XCTAssertEqual(sections[1].section, "Section 1") 76 | XCTAssertEqual(sections[2].section, "Section 2") 77 | XCTAssertEqual(sections[indexPath: IndexPath(row: 0, section: 0)], .title) 78 | XCTAssertEqual(sections[indexPath: IndexPath(row: 5, section: 0)], .body) 79 | XCTAssertEqual(sections[indexPath: IndexPath(row: 0, section: 1)], .image) 80 | XCTAssertEqual(sections[indexPath: IndexPath(row: 2, section: 1)], .image) 81 | XCTAssertEqual(sections[indexPath: IndexPath(row: 0, section: 2)], .title) 82 | } 83 | 84 | func testIndexPathRoleFromTable() { 85 | let tester = TableViewTester(sections: sections) 86 | XCTAssertEqual(tester.tableView.role(ofRow: IndexPath(row: 0, section: 0)), .first) 87 | XCTAssertEqual(tester.tableView.role(ofRow: IndexPath(row: 1, section: 0)), .middle) 88 | XCTAssertEqual(tester.tableView.role(ofRow: IndexPath(row: 2, section: 0)), .middle) 89 | XCTAssertEqual(tester.tableView.role(ofRow: IndexPath(row: 3, section: 0)), .middle) 90 | XCTAssertEqual(tester.tableView.role(ofRow: IndexPath(row: 4, section: 0)), .middle) 91 | XCTAssertEqual(tester.tableView.role(ofRow: IndexPath(row: 5, section: 0)), .last) 92 | 93 | XCTAssertEqual(tester.tableView.role(ofRow: IndexPath(row: 0, section: 1)), .first) 94 | XCTAssertEqual(tester.tableView.role(ofRow: IndexPath(row: 1, section: 1)), .middle) 95 | XCTAssertEqual(tester.tableView.role(ofRow: IndexPath(row: 2, section: 1)), .last) 96 | 97 | XCTAssertEqual(tester.tableView.role(ofRow: IndexPath(row: 0, section: 2)), .only) 98 | } 99 | 100 | func testIndexPathRoleFromIndexPath() { 101 | XCTAssertEqual(IndexPath(row: 0, section: 0).role(inSectionWithNumberOfRows: 6), .first) 102 | XCTAssertEqual(IndexPath(row: 1, section: 0).role(inSectionWithNumberOfRows: 6), .middle) 103 | XCTAssertEqual(IndexPath(row: 2, section: 0).role(inSectionWithNumberOfRows: 6), .middle) 104 | XCTAssertEqual(IndexPath(row: 3, section: 0).role(inSectionWithNumberOfRows: 6), .middle) 105 | XCTAssertEqual(IndexPath(row: 4, section: 0).role(inSectionWithNumberOfRows: 6), .middle) 106 | XCTAssertEqual(IndexPath(row: 5, section: 0).role(inSectionWithNumberOfRows: 6), .last) 107 | 108 | XCTAssertEqual(IndexPath(row: 0, section: 1).role(inSectionWithNumberOfRows: 3), .first) 109 | XCTAssertEqual(IndexPath(row: 1, section: 1).role(inSectionWithNumberOfRows: 3), .middle) 110 | XCTAssertEqual(IndexPath(row: 2, section: 1).role(inSectionWithNumberOfRows: 3), .last) 111 | 112 | XCTAssertEqual(IndexPath(row: 0, section: 2).role(inSectionWithNumberOfRows: 1), .only) 113 | } 114 | 115 | 116 | } 117 | 118 | #endif 119 | -------------------------------------------------------------------------------- /Pod/Classes/Views/Textview/UITextView+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UITextView+Extensions.swift 3 | // Swiftilities 4 | // 5 | // Created by Derek Ostrander on 6/8/16. 6 | // Copyright © 2016 Raizlabs. All rights reserved. 7 | // 8 | 9 | #if canImport(UIKit) 10 | import UIKit 11 | 12 | extension PlaceholderConfigurable where Self: UITextView { 13 | fileprivate func xConstraint() -> NSLayoutConstraint { 14 | let constraint: NSLayoutConstraint = constraints 15 | .filter({ (constraint: NSLayoutConstraint) -> Bool in 16 | return constraint.firstAttribute == .left && 17 | constraint.secondAttribute == .left && 18 | (constraint.firstItem === self || constraint.secondItem === self) 19 | }) 20 | .first ?? placeholderLabel.leftAnchor.constraint(equalTo: leftAnchor) 21 | constraint.isActive = true 22 | 23 | return constraint 24 | } 25 | 26 | fileprivate func yConstraint() -> NSLayoutConstraint { 27 | let constraint: NSLayoutConstraint = constraints 28 | .filter({ (constraint: NSLayoutConstraint) -> Bool in 29 | return constraint.firstAttribute == .top && 30 | constraint.secondAttribute == .top && 31 | (constraint.firstItem === self || constraint.secondItem === self) 32 | }).first ?? placeholderLabel.topAnchor.constraint(equalTo: topAnchor) 33 | constraint.isActive = true 34 | 35 | return constraint 36 | } 37 | 38 | var placeholderConstraints: OriginConstraints { 39 | return (xConstraint(), yConstraint()) 40 | } 41 | 42 | var placeholderPosition: CGPoint { 43 | return CGPoint(x: textContainerInset.left + textContainer.lineFragmentPadding, 44 | y: textContainerInset.top) 45 | } 46 | 47 | func adjustPlaceholder() { 48 | placeholderLabel.isHidden = text.count > 0 49 | 50 | let position = placeholderPosition 51 | placeholderConstraints.x?.constant = position.x 52 | placeholderConstraints.y?.constant = position.y 53 | } 54 | } 55 | 56 | extension HeightAutoAdjustable where Self: UITextView { 57 | fileprivate var bottomOffset: CGPoint { 58 | let verticalInset = abs(textContainerInset.top - textContainerInset.bottom) 59 | return CGPoint(x: 0.0, 60 | y: contentSize.height - self.bounds.height + verticalInset) 61 | } 62 | 63 | var intrinsicContentHeight: CGFloat { 64 | let height = sizeThatFits(CGSize(width: bounds.width, height: CGFloat.greatestFiniteMagnitude)).height 65 | guard let font = font else { 66 | return height 67 | } 68 | 69 | let minimum = textContainerInset.top + self.textContainerInset.bottom + font.lineHeight 70 | return max(height, minimum) 71 | } 72 | 73 | // Attempts to find the apporpriate constraint and creates one if needed. 74 | func heightConstraint() -> NSLayoutConstraint { 75 | let constraint: NSLayoutConstraint = constraints 76 | .filter({ (constraint: NSLayoutConstraint) -> Bool in 77 | return constraint.firstAttribute == .height && 78 | constraint.firstItem === self && 79 | constraint.isActive && 80 | constraint.multiplier == 1.0 81 | }).first ?? heightAnchor.constraint(equalToConstant: intrinsicContentHeight) 82 | 83 | if !constraint.isActive { 84 | constraint.priority = heightPriority 85 | constraint.isActive = true 86 | setNeedsLayout() 87 | } 88 | 89 | return constraint 90 | } 91 | 92 | func adjustHeight() { 93 | let height = intrinsicContentHeight 94 | guard height > 0 && heightConstraint().constant != height else { return } 95 | heightConstraint().constant = height 96 | 97 | setNeedsLayout() 98 | 99 | let animated = animationDelegate?.shouldAnimateHeightChange(self) ?? false 100 | guard let container = animationDelegate?.containerToLayout(forTextView: self), animated else { 101 | scrollToBottom(animated) 102 | return 103 | } 104 | 105 | let duration = animationDelegate?.animationDuration(self) ?? 0.1 106 | UIView.animate(withDuration: duration, animations: { 107 | container.layoutIfNeeded() 108 | if self.bottomOffset.y < (self.font?.lineHeight ?? 0.0) { 109 | self.scrollToBottom(animated) 110 | } 111 | }) 112 | } 113 | 114 | func scrollToBottom(_ animated: Bool) { 115 | setContentOffset(bottomOffset, animated: animated) 116 | } 117 | } 118 | 119 | #endif 120 | --------------------------------------------------------------------------------