├── .swift-version ├── utopia ├── Assets │ └── .gitkeep └── Source │ ├── .gitkeep │ ├── Types │ └── None.swift │ ├── Utilities │ ├── SimpleError.swift │ ├── Literals.swift │ ├── Optional+ext.swift │ ├── Bundle.swift │ ├── Associated.swift │ ├── Time.swift │ ├── Comparable+ext.swift │ ├── NSAttributedStrings+ext.swift │ ├── Require.swift │ ├── ScrollDismission.swift │ ├── AsyncOperation.swift │ ├── DateUtilities.swift │ ├── Math.swift │ ├── Then.swift │ └── TimingFunction.swift │ ├── UIKit │ ├── Extensions │ │ ├── UIScrollView+ext.swift │ │ ├── UIButton+ext.swift │ │ ├── UITextField+ext.swift │ │ ├── UINavigationController+ext.swift │ │ ├── UITableView+Reusable.swift │ │ ├── UIStackView+ext.swift │ │ ├── UIWindow+ext.swift │ │ ├── UIView+ext.swift │ │ ├── UIGestureRecognizer+ext.swift │ │ ├── UIAlertController+ext.swift │ │ ├── UICollectionView+Reusable.swift │ │ ├── UIViewController+ext.swift │ │ ├── CoreGraphics+ext.swift │ │ ├── UIImage+Gradient.swift │ │ ├── UIButton+layout.swift │ │ └── UIImage+ext.swift │ ├── Snapshot.swift │ ├── StatusBarAppearance.swift │ ├── CollectionLayout │ │ ├── PinnedHeaderFlowLayout.swift │ │ ├── PaginationFlowLayout.swift │ │ └── SnappingFlowLayout.swift │ ├── Keyboard │ │ ├── InputVisibilityController │ │ │ └── TouchRecognizer.swift │ │ └── KeyboardObserver.swift │ ├── Gestures │ │ └── GestureRecognizerDelegate.swift │ ├── BarButtons.swift │ ├── LayoutGuides.swift │ ├── Animations │ │ └── TouchInteraction.swift │ ├── Display.swift │ ├── Toggler.swift │ ├── UISearchUtilities.swift │ └── Views │ │ └── GradientView.swift │ ├── OptimizationTricks │ ├── ImageSettable.swift │ ├── SwiftyImageView.swift │ └── SwiftyImage.swift │ ├── Haptic │ ├── Haptic.swift │ └── Hapticable.swift │ ├── Transitions │ ├── TransitioningDelegate.swift │ ├── AlphaTransition.swift │ ├── UIView+transitions.swift │ ├── ScaleModalTransitioning.swift │ └── InteractiveDismiss.swift │ ├── Validation │ ├── StringValidators.swift │ ├── ValidatableView.swift │ └── Validation.swift │ ├── Signals │ ├── AssociatedObject.swift │ ├── UIBarButtonItem+Signals.swift │ ├── SignalSubscription.swift │ ├── UIControl+Signals.swift │ └── Signal.swift │ ├── SwiftStdLib+ext │ ├── String+ext.swift │ ├── Random.swift │ └── Collections+ext.swift │ └── CodingDeconding │ └── CodableUtilities.swift ├── _Pods.xcodeproj ├── Example ├── Podfile ├── Pods │ ├── Target Support Files │ │ ├── utopia │ │ │ ├── utopia.modulemap │ │ │ ├── utopia-dummy.m │ │ │ ├── utopia-prefix.pch │ │ │ ├── utopia-umbrella.h │ │ │ ├── utopia.xcconfig │ │ │ ├── Info.plist │ │ │ └── utopia-Info.plist │ │ └── Pods-utopia_Example │ │ │ ├── Pods-utopia_Example.modulemap │ │ │ ├── Pods-utopia_Example-dummy.m │ │ │ ├── Pods-utopia_Example-umbrella.h │ │ │ ├── Pods-utopia_Example.debug.xcconfig │ │ │ ├── Pods-utopia_Example.release.xcconfig │ │ │ ├── Info.plist │ │ │ ├── Pods-utopia_Example-Info.plist │ │ │ ├── Pods-utopia_Example-acknowledgements.markdown │ │ │ ├── Pods-utopia_Example-acknowledgements.plist │ │ │ ├── Pods-utopia_Example-resources.sh │ │ │ └── Pods-utopia_Example-frameworks.sh │ ├── Manifest.lock │ └── Local Podspecs │ │ └── utopia.podspec.json ├── utopia.xcodeproj │ ├── project.xcworkspace │ │ └── contents.xcworkspacedata │ └── xcshareddata │ │ └── xcschemes │ │ └── utopia-Example.xcscheme ├── utopia.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── Podfile.lock └── utopia │ ├── AppDelegate.swift │ ├── Images.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json │ ├── Info.plist │ └── Base.lproj │ └── LaunchScreen.xib ├── utopia.podspec ├── .travis.yml ├── README.md ├── .gitignore └── LICENSE /.swift-version: -------------------------------------------------------------------------------- 1 | 4.2 2 | -------------------------------------------------------------------------------- /utopia/Assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /utopia/Source/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /_Pods.xcodeproj: -------------------------------------------------------------------------------- 1 | Example/Pods/Pods.xcodeproj -------------------------------------------------------------------------------- /Example/Podfile: -------------------------------------------------------------------------------- 1 | platform :ios, '10.0' 2 | 3 | use_frameworks! 4 | 5 | target 'utopia_Example' do 6 | pod 'utopia', :path => '../' 7 | end 8 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/utopia/utopia.modulemap: -------------------------------------------------------------------------------- 1 | framework module utopia { 2 | umbrella header "utopia-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/utopia/utopia-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_utopia : NSObject 3 | @end 4 | @implementation PodsDummy_utopia 5 | @end 6 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-utopia_Example/Pods-utopia_Example.modulemap: -------------------------------------------------------------------------------- 1 | framework module Pods_utopia_Example { 2 | umbrella header "Pods-utopia_Example-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-utopia_Example/Pods-utopia_Example-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_Pods_utopia_Example : NSObject 3 | @end 4 | @implementation PodsDummy_Pods_utopia_Example 5 | @end 6 | -------------------------------------------------------------------------------- /Example/utopia.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /utopia/Source/Types/None.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public let none = None() 4 | 5 | public struct None {} 6 | 7 | extension None: Hashable { 8 | 9 | public func hash(into hasher: inout Hasher) { } 10 | } 11 | 12 | public func == (lhs: None, rhs: None) -> Bool { return true } 13 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/utopia/utopia-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/utopia.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Example/utopia.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /utopia/Source/Utilities/SimpleError.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct SimpleError: Error, LocalizedError { 4 | let message: String 5 | 6 | public init(_ message: String) { 7 | self.message = message 8 | } 9 | 10 | public var errorDescription: String? { 11 | return message 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Example/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - utopia (0.2.1) 3 | 4 | DEPENDENCIES: 5 | - utopia (from `../`) 6 | 7 | EXTERNAL SOURCES: 8 | utopia: 9 | :path: "../" 10 | 11 | SPEC CHECKSUMS: 12 | utopia: 5f8b06c09df6ecc26f8f64a01bce85b887cfb4ab 13 | 14 | PODFILE CHECKSUM: 632947ed90777377758aa9384f691613d327ceb4 15 | 16 | COCOAPODS: 1.8.4 17 | -------------------------------------------------------------------------------- /Example/Pods/Manifest.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - utopia (0.2.1) 3 | 4 | DEPENDENCIES: 5 | - utopia (from `../`) 6 | 7 | EXTERNAL SOURCES: 8 | utopia: 9 | :path: "../" 10 | 11 | SPEC CHECKSUMS: 12 | utopia: 5f8b06c09df6ecc26f8f64a01bce85b887cfb4ab 13 | 14 | PODFILE CHECKSUM: 632947ed90777377758aa9384f691613d327ceb4 15 | 16 | COCOAPODS: 1.8.4 17 | -------------------------------------------------------------------------------- /utopia/Source/Utilities/Literals.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | 4 | extension URL: ExpressibleByStringLiteral { 5 | // By using 'StaticString' we disable string interpolation, for safety 6 | public init(stringLiteral value: StaticString) { 7 | self = URL(string: "\(value)").require(hint: "Invalid URL string literal: \(value)") 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Example/utopia/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | @UIApplicationMain 4 | class AppDelegate: UIResponder, UIApplicationDelegate { 5 | 6 | var window: UIWindow? 7 | 8 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 9 | return true 10 | } 11 | } 12 | 13 | -------------------------------------------------------------------------------- /utopia/Source/UIKit/Extensions/UIScrollView+ext.swift: -------------------------------------------------------------------------------- 1 | import UIKit.UIScrollView 2 | 3 | extension UIScrollView { 4 | 5 | func scrollToBottom() { 6 | layoutIfNeeded() 7 | let minimumYOffset = -max(Display.navbarSize, contentInset.top) 8 | let y = max(minimumYOffset, bounds.minY + contentSize.height + contentInset.top - bounds.height) 9 | contentOffset = CGPoint(x: 0, y: y) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/utopia/utopia-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 utopiaVersionNumber; 15 | FOUNDATION_EXPORT const unsigned char utopiaVersionString[]; 16 | 17 | -------------------------------------------------------------------------------- /utopia/Source/Utilities/Optional+ext.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Optional { 4 | /** 5 | Attempts to unwrap the optional, and executes the closure if a value exists 6 | 7 | - parameter block: The closure to execute if a value is found 8 | */ 9 | public func unwrap(_ block: (Wrapped) throws -> Void) rethrows { 10 | if let value = self { 11 | try block(value) 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-utopia_Example/Pods-utopia_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_utopia_ExampleVersionNumber; 15 | FOUNDATION_EXPORT const unsigned char Pods_utopia_ExampleVersionString[]; 16 | 17 | -------------------------------------------------------------------------------- /utopia/Source/Utilities/Bundle.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public extension Bundle { 4 | 5 | var appName: String { 6 | return infoDictionary?["CFBundleName"] as! String 7 | } 8 | 9 | var bundleId: String { 10 | return bundleIdentifier! 11 | } 12 | 13 | var versionNumber: String { 14 | return infoDictionary?["CFBundleShortVersionString"] as! String 15 | } 16 | 17 | var buildNumber: String { 18 | return infoDictionary?["CFBundleVersion"] as! String 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /utopia/Source/UIKit/Extensions/UIButton+ext.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | 5 | extension UIButton { 6 | /** 7 | Sets the background color to use for the specified button state. 8 | 9 | - parameter color: The background color to use for the specified state. 10 | - parameter state: The state that uses the specified image. 11 | */ 12 | public func setBackgroundColor(_ color: UIColor, for state: UIControl.State) { 13 | setBackgroundImage(UIImage(color: color), for: state) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /utopia.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'utopia' 3 | s.version = '0.2.1' 4 | s.summary = 'Common utilities for Swift projects' 5 | s.homepage = 'https://github.com/Ramotion/utopia' 6 | s.license = { :type => 'MIT', :file => 'LICENSE' } 7 | s.author = { 'Ramotion' => 'igor.k@ramotion.com' } 8 | s.source = { :git => 'https://github.com/Ramotion/utopia.git', :tag => s.version.to_s } 9 | s.ios.deployment_target = '10.0' 10 | s.source_files = 'utopia/Source/**/*' 11 | end 12 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/utopia/utopia.xcconfig: -------------------------------------------------------------------------------- 1 | CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/utopia 2 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 3 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS 4 | PODS_BUILD_DIR = ${BUILD_DIR} 5 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 6 | PODS_ROOT = ${SRCROOT} 7 | PODS_TARGET_SRCROOT = ${PODS_ROOT}/../.. 8 | PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} 9 | SKIP_INSTALL = YES 10 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # references: 2 | # * http://www.objc.io/issue-6/travis-ci.html 3 | # * https://github.com/supermarin/xcpretty#usage 4 | 5 | osx_image: xcode7.3 6 | language: objective-c 7 | # cache: cocoapods 8 | # podfile: Example/Podfile 9 | # before_install: 10 | # - gem install cocoapods # Since Travis is not always on latest version 11 | # - pod install --project-directory=Example 12 | script: 13 | - set -o pipefail && xcodebuild test -enableCodeCoverage YES -workspace Example/utopia.xcworkspace -scheme utopia-Example -sdk iphonesimulator9.3 ONLY_ACTIVE_ARCH=NO | xcpretty 14 | - pod lib lint 15 | -------------------------------------------------------------------------------- /Example/Pods/Local Podspecs/utopia.podspec.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "utopia", 3 | "version": "0.2.1", 4 | "summary": "Common utilities for Swift projects", 5 | "homepage": "https://github.com/Ramotion/utopia", 6 | "license": { 7 | "type": "MIT", 8 | "file": "LICENSE" 9 | }, 10 | "authors": { 11 | "Dmitriy Kalachev": "dima.k@ramotion.com" 12 | "Ramotion": "igor.k@ramotion.com" 13 | }, 14 | "source": { 15 | "git": "https://github.com/Ramotion/utopia.git", 16 | "tag": "0.2.1" 17 | }, 18 | "platforms": { 19 | "ios": "10.0" 20 | }, 21 | "source_files": "utopia/Source/**/*" 22 | } 23 | -------------------------------------------------------------------------------- /utopia/Source/Utilities/Associated.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | private class Associated: NSObject { 4 | let value: T 5 | init(_ value: T) { 6 | self.value = value 7 | } 8 | } 9 | 10 | public protocol Associable {} 11 | 12 | public extension Associable where Self: AnyObject { 13 | 14 | func getAssociatedObject(_ key: UnsafeRawPointer) -> T? { 15 | return (objc_getAssociatedObject(self, key) as? Associated).map { $0.value } 16 | } 17 | 18 | func setAssociatedObject(_ key: UnsafeRawPointer, _ value: T?) { 19 | objc_setAssociatedObject(self, key, value.map { Associated($0) }, .OBJC_ASSOCIATION_RETAIN) 20 | } 21 | 22 | } 23 | 24 | extension NSObject: Associable {} 25 | -------------------------------------------------------------------------------- /utopia/Source/OptimizationTricks/ImageSettable.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | 4 | public extension UIView { 5 | var backgroundImage: UIImage? { 6 | get { 7 | guard let obj = layer.contents else { return nil } 8 | return UIImage(cgImage: obj as! CGImage) 9 | } 10 | set { 11 | layer.contents = newValue?.cgImage 12 | if newValue?.isOpaque == true { 13 | isOpaque = true 14 | } 15 | else { 16 | updateOpaque() 17 | } 18 | } 19 | } 20 | } 21 | 22 | 23 | public extension UIView { 24 | final func updateOpaque() { 25 | if let color = backgroundColor, color.rgba.a == 1.0, alpha == 1.0 { 26 | isOpaque = true 27 | } 28 | else { 29 | isOpaque = false 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /utopia/Source/UIKit/Extensions/UITextField+ext.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | @IBDesignable 4 | extension UITextField { 5 | 6 | @IBInspectable var leftPaddingWidth: CGFloat { 7 | get { 8 | return leftView?.frame.size.width ?? 0 9 | } 10 | set { 11 | let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: newValue, height: frame.size.height)) 12 | leftView = paddingView 13 | leftViewMode = .always 14 | } 15 | } 16 | 17 | @IBInspectable var rigthPaddingWidth: CGFloat { 18 | get { 19 | return rightView?.frame.size.width ?? 0 20 | } 21 | set { 22 | let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: newValue, height: frame.size.height)) 23 | rightView = paddingView 24 | rightViewMode = .always 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /utopia/Source/UIKit/Extensions/UINavigationController+ext.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UINavigationController+ext.swift 3 | // utopia 4 | // 5 | // Created by Dmitriy Kalachev on 4/19/18. 6 | // Copyright © 2018 Ramotion. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public extension UINavigationController { 12 | 13 | func pushViewController(_ vc: UIViewController) { 14 | pushViewController(vc, animated: true) 15 | } 16 | 17 | func replace(_ vc: UIViewController, with replacementVC: UIViewController, animated: Bool = true) { 18 | guard let index = self.viewControllers.firstIndex(of: vc) 19 | else { return } 20 | 21 | var viewControllers = self.viewControllers 22 | viewControllers[index] = replacementVC 23 | setViewControllers(viewControllers, animated: animated) 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-utopia_Example/Pods-utopia_Example.debug.xcconfig: -------------------------------------------------------------------------------- 1 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES 2 | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/utopia" 3 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 4 | HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/utopia/utopia.framework/Headers" 5 | LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' 6 | OTHER_LDFLAGS = $(inherited) -framework "utopia" 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-utopia_Example/Pods-utopia_Example.release.xcconfig: -------------------------------------------------------------------------------- 1 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES 2 | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/utopia" 3 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 4 | HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/utopia/utopia.framework/Headers" 5 | LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' 6 | OTHER_LDFLAGS = $(inherited) -framework "utopia" 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # utopia 2 | 3 | [![Version](https://img.shields.io/cocoapods/v/utopia.svg?style=flat)](http://cocoapods.org/pods/utopia) 4 | [![License](https://img.shields.io/cocoapods/l/utopia.svg?style=flat)](http://cocoapods.org/pods/utopia) 5 | [![Platform](https://img.shields.io/cocoapods/p/utopia.svg?style=flat)](http://cocoapods.org/pods/utopia) 6 | 7 | ## Example 8 | 9 | To run the example project, clone the repo, and run `pod install` from the Example directory first. 10 | 11 | ## Requirements 12 | 13 | ## Installation 14 | 15 | utopia is available through [CocoaPods](http://cocoapods.org). To install 16 | it, simply add the following line to your Podfile: 17 | 18 | ```ruby 19 | pod 'utopia' 20 | ``` 21 | 22 | ## Author 23 | 24 | Ramotion 25 | 26 | ## License 27 | 28 | utopia is available under the MIT license. See the LICENSE file for more info. 29 | -------------------------------------------------------------------------------- /utopia/Source/Haptic/Haptic.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | public enum Haptic { 5 | case impact(UIImpactFeedbackGenerator.FeedbackStyle) 6 | case notification(UINotificationFeedbackGenerator.FeedbackType) 7 | case selection 8 | 9 | public func generate() { 10 | guard #available(iOS 10, *) else { return } 11 | 12 | switch self { 13 | case .impact(let style): 14 | let generator = UIImpactFeedbackGenerator(style: style) 15 | generator.prepare() 16 | generator.impactOccurred() 17 | case .notification(let type): 18 | let generator = UINotificationFeedbackGenerator() 19 | generator.prepare() 20 | generator.notificationOccurred(type) 21 | case .selection: 22 | let generator = UISelectionFeedbackGenerator() 23 | generator.prepare() 24 | generator.selectionChanged() 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /utopia/Source/UIKit/Extensions/UITableView+Reusable.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | public extension UITableView { 4 | 5 | func registerCell(_ type: T.Type, withIdentifier reuseIdentifier: String = T.reuseIdentifier) { 6 | register(T.self, forCellReuseIdentifier: reuseIdentifier) 7 | } 8 | 9 | func dequeueCell(_ type: T.Type = T.self, withIdentifier reuseIdentifier: String = T.reuseIdentifier, 10 | for indexPath: IndexPath) -> T { 11 | 12 | guard let cell = dequeueReusableCell(withIdentifier: reuseIdentifier, for: indexPath) as? T else { 13 | fatalError("Unknown cell type (\(T.self)) for reuse identifier: \(reuseIdentifier)") 14 | } 15 | return cell 16 | } 17 | } 18 | 19 | public extension UITableViewCell { 20 | 21 | static var reuseIdentifier: String { 22 | return String(describing: self) 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /.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 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 26 | # Carthage/Checkouts 27 | 28 | Carthage/Build 29 | 30 | # We recommend against adding the Pods directory to your .gitignore. However 31 | # you should judge for yourself, the pros and cons are mentioned at: 32 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control 33 | # 34 | # Note: if you ignore the Pods directory, make sure to uncomment 35 | # `pod install` in .travis.yml 36 | # 37 | # Pods/ 38 | -------------------------------------------------------------------------------- /utopia/Source/Transitions/TransitioningDelegate.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | public final class TransitioningDelegate: NSObject, UIViewControllerTransitioningDelegate where T: UIViewControllerAnimatedTransitioning, R: UIViewControllerAnimatedTransitioning { 5 | 6 | public let presentation: T 7 | public let dismiss: R 8 | 9 | public init(present: T, dismiss: R) { 10 | self.presentation = present 11 | self.dismiss = dismiss 12 | } 13 | 14 | public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { 15 | return presentation 16 | } 17 | 18 | public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { 19 | return dismiss 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /utopia/Source/Validation/StringValidators.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StringValidators.swift 3 | // Vendefy 4 | // 5 | // Created by Dmitriy Kalachev on 4/17/18. 6 | // Copyright © 2018 Ramotion. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | public extension Validator where Value == String { 13 | static func pattern(_ pattern: String, errorMessage: String) -> Validator { 14 | return .block(message: errorMessage) { 15 | $0.trimmed.matches(regex: pattern) 16 | } 17 | } 18 | 19 | static func empty() -> Validator { 20 | return .block() { $0.isEmpty } 21 | } 22 | } 23 | 24 | public extension String { 25 | var trimmed: String { 26 | return trimmingCharacters(in: .whitespacesAndNewlines) 27 | } 28 | 29 | func matches(regex pattern: String) -> Bool { 30 | return range(of: pattern, options: .regularExpression) == startIndex.. UIStackView { 14 | return vertical(arrangedSubviews) 15 | } 16 | 17 | static func vertical(_ arrangedSubviews: [UIView]) -> UIStackView { 18 | return UIStackView(arrangedSubviews: arrangedSubviews).then { 19 | $0.axis = .vertical 20 | } 21 | } 22 | 23 | func clearArrangedSubviews() { 24 | arrangedSubviews.forEach { 25 | removeArrangedSubview($0) 26 | $0.removeFromSuperview() 27 | } 28 | } 29 | 30 | func addArrangedSubviews(_ subviews: [UIView]) { 31 | subviews.forEach { 32 | addArrangedSubview($0) 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/utopia/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.1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/utopia/utopia-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.2.1 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-utopia_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/Pods-utopia_Example/Pods-utopia_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 | -------------------------------------------------------------------------------- /utopia/Source/Signals/AssociatedObject.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2014 - 2017 Tuomas Artman. All rights reserved. 3 | // 4 | 5 | import ObjectiveC 6 | 7 | func setAssociatedObject(_ object: AnyObject, value: T, associativeKey: UnsafeRawPointer, policy: objc_AssociationPolicy) { 8 | objc_setAssociatedObject(object, associativeKey, value, policy) 9 | } 10 | 11 | func getAssociatedObject(_ object: AnyObject, associativeKey: UnsafeRawPointer) -> T? { 12 | if let valueAsType = objc_getAssociatedObject(object, associativeKey) as? T { 13 | return valueAsType 14 | } else { 15 | return nil 16 | } 17 | } 18 | 19 | func getOrCreateAssociatedObject(_ object: AnyObject, associativeKey: UnsafeRawPointer, defaultValue:T, policy: objc_AssociationPolicy) -> T { 20 | if let valueAsType: T = getAssociatedObject(object, associativeKey: associativeKey) { 21 | return valueAsType 22 | } 23 | setAssociatedObject(object, value: defaultValue, associativeKey: associativeKey, policy: policy) 24 | return defaultValue; 25 | } 26 | -------------------------------------------------------------------------------- /utopia/Source/SwiftStdLib+ext/String+ext.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | public extension Optional where Wrapped == String { 5 | var isNilOrEmpty: Bool { 6 | switch self { 7 | case let string?: 8 | return string.isEmpty 9 | case nil: 10 | return true 11 | } 12 | } 13 | } 14 | 15 | 16 | extension String { 17 | 18 | public func size(withFont font: UIFont, constrainedToWidth width: CGFloat? = nil) -> CGSize { 19 | //TODO: update this method after Swift 4.2 migration 20 | 21 | let size: CGSize 22 | if let width = width { 23 | size = CGSize(width: width, height: CGFloat.greatestFiniteMagnitude) 24 | } else { 25 | size = CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude) 26 | } 27 | 28 | let attributes: [NSAttributedString.Key: Any] = [ NSAttributedString.Key.font: font ] 29 | let result = (self as NSString).boundingRect(with: size,options: [ .usesLineFragmentOrigin ], attributes: attributes, context: nil).size 30 | 31 | return result 32 | } 33 | } 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /Example/utopia/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ios-marketing", 45 | "size" : "1024x1024", 46 | "scale" : "1x" 47 | } 48 | ], 49 | "info" : { 50 | "version" : 1, 51 | "author" : "xcode" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 Dmitriy Kalachev 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /utopia/Source/Utilities/Time.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | 4 | extension TimeInterval { 5 | public static var oneFrame: TimeInterval { 6 | return 1/60.0 7 | } 8 | 9 | public static var halfSecond: TimeInterval { 10 | return 1/2.0 11 | } 12 | 13 | public static var oneSecond: TimeInterval { 14 | return 1 15 | } 16 | 17 | public static var oneDay: TimeInterval { 18 | return 60 * 60 * 24 19 | } 20 | 21 | public static var twoDays: TimeInterval { 22 | return 60 * 60 * 24 * 2 23 | } 24 | 25 | public static var treeDays: TimeInterval { 26 | return 60 * 60 * 24 * 3 27 | } 28 | 29 | public static var oneWeek: TimeInterval { 30 | return 60 * 60 * 24 * 7 31 | } 32 | 33 | public static var oneMonth: TimeInterval { 34 | return 60 * 60 * 24 * 30 35 | } 36 | 37 | public static var oneYear: TimeInterval { 38 | return 60 * 60 * 24 * 366 39 | } 40 | } 41 | 42 | public func delay(_ delay: Double, _ closure:@escaping ()->()) { 43 | DispatchQueue.main.asyncAfter( 44 | deadline: DispatchTime.now() + Double(Int64(delay * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC), execute: closure) 45 | } 46 | -------------------------------------------------------------------------------- /Example/utopia/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /utopia/Source/Utilities/Comparable+ext.swift: -------------------------------------------------------------------------------- 1 | extension Comparable { 2 | 3 | /** 4 | Bound the current value between a minimum and maximum value 5 | 6 | - parameter min: The minimum possible value 7 | - parameter max: The maximum possible value 8 | 9 | - returns: The current value bounded between a minimum and maximum value 10 | */ 11 | public func limited(min: Self, max: Self) -> Self { 12 | var value = self 13 | value.limit(min: min, max: max) 14 | return value 15 | } 16 | 17 | /** 18 | Bound the current value between a minimum and maximum value 19 | 20 | - parameter min: The minimum possible value 21 | - parameter max: The maximum possible value 22 | 23 | - returns: The current value bounded between a minimum and maximum value 24 | */ 25 | public func limited(_ min: Self, _ max: Self) -> Self { 26 | return limited(min: min, max: max) 27 | } 28 | 29 | /** 30 | Bound self between a minimum and maximum value, in place 31 | 32 | - parameter min: The minimum possible value 33 | - parameter max: The maximum possible value 34 | */ 35 | public mutating func limit(min: Self, max: Self) { 36 | self = Swift.max(Swift.min(self, max), min) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /utopia/Source/UIKit/Snapshot.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | 5 | public extension UIView { 6 | 7 | func snapshotImage(opaque: Bool = true, scale: CGFloat = UIScreen.main.scale * 2, afterScreenUpdates: Bool = false) -> UIImage? { 8 | UIGraphicsBeginImageContextWithOptions(bounds.size, opaque, scale) 9 | drawHierarchy(in: bounds, afterScreenUpdates: afterScreenUpdates) 10 | let snapshotImage = UIGraphicsGetImageFromCurrentImageContext() 11 | UIGraphicsEndImageContext() 12 | return snapshotImage 13 | } 14 | 15 | func snapshotView(opaque: Bool = true, scale: CGFloat = UIScreen.main.scale * 2, afterScreenUpdates: Bool = false) -> UIView? { 16 | if let snapshotImage = snapshotImage(opaque: opaque, scale: scale, afterScreenUpdates: afterScreenUpdates) { 17 | return UIImageView(image: snapshotImage) 18 | } else { 19 | return nil 20 | } 21 | } 22 | 23 | func snapshotLayer(opaque: Bool = true, scale: CGFloat = UIScreen.main.scale * 2) -> UIImage? { 24 | 25 | UIGraphicsBeginImageContextWithOptions(bounds.size, opaque, scale) 26 | guard let context = UIGraphicsGetCurrentContext() else { return nil } 27 | layer.render(in: context) 28 | let image = UIGraphicsGetImageFromCurrentImageContext() 29 | UIGraphicsEndImageContext() 30 | 31 | return image 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /utopia/Source/Haptic/Hapticable.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | private var hapticKey: Void? 5 | private var eventKey: Void? 6 | 7 | public protocol Hapticable: class { 8 | func trigger(_ sender: Any) 9 | } 10 | 11 | extension Hapticable where Self: UIButton { 12 | 13 | public var isHaptic: Bool { 14 | get { 15 | guard let actions = actions(forTarget: self, forControlEvent: hapticControlEvents ?? .touchDown) else { return false } 16 | return !actions.filter { $0 == #selector(trigger).description }.isEmpty 17 | } 18 | set { 19 | if newValue { 20 | addTarget(self, action: #selector(trigger), for: hapticControlEvents ?? .touchDown) 21 | } else { 22 | removeTarget(self, action: #selector(trigger), for: hapticControlEvents ?? .touchDown) 23 | } 24 | } 25 | } 26 | 27 | public var hapticType: Haptic? { 28 | get { return getAssociatedObject(&hapticKey) } 29 | set { setAssociatedObject(&hapticKey, newValue) } 30 | } 31 | 32 | public var hapticControlEvents: UIControl.Event? { 33 | get { return getAssociatedObject(&eventKey) } 34 | set { setAssociatedObject(&eventKey, newValue) } 35 | } 36 | 37 | } 38 | 39 | extension UIButton: Hapticable { 40 | 41 | @objc public func trigger(_ sender: Any) { 42 | hapticType?.generate() 43 | } 44 | 45 | } 46 | 47 | 48 | -------------------------------------------------------------------------------- /utopia/Source/Utilities/NSAttributedStrings+ext.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | extension NSMutableAttributedString { 5 | 6 | public func addAttributes(for string: String, attributes: [NSAttributedString.Key : Any]) -> Self { 7 | let range = (self.string as NSString).range(of: string) 8 | addAttributes(attributes, range: range) 9 | return self 10 | } 11 | 12 | public func setAsLink(textToFind:String, linkURL:String, color: UIColor, font: UIFont? = nil) -> Self { 13 | let foundRange = self.mutableString.range(of: textToFind) 14 | if foundRange.location != NSNotFound { 15 | self.addAttribute(NSAttributedString.Key.link, value: linkURL, range: foundRange) 16 | self.addAttribute(NSAttributedString.Key.foregroundColor, value: color, range: foundRange) 17 | if let font = font { self.addAttribute(NSAttributedString.Key.font, value: font, range: foundRange) } 18 | } 19 | return self 20 | } 21 | } 22 | 23 | extension NSAttributedString { 24 | 25 | public func size(constrainedToWidth width: CGFloat? = nil) -> CGSize { 26 | 27 | let size: CGSize = CGSize(width: width ?? CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude) 28 | let result = boundingRect(with: size, options: [ .usesLineFragmentOrigin, .usesFontLeading ], context: nil).size 29 | return result 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /utopia/Source/UIKit/StatusBarAppearance.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | extension UIViewController { 5 | 6 | private static var statusBarSubstrateKey: Int = 0 7 | 8 | public var statusBarSubstrate: UIView? { 9 | get { 10 | return objc_getAssociatedObject(self, &UIViewController.statusBarSubstrateKey) as? UIView 11 | } 12 | set { 13 | objc_setAssociatedObject(self, &UIViewController.statusBarSubstrateKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN) 14 | } 15 | } 16 | 17 | public func setupStatusBarSubstrate(color: UIColor, force: Bool = false) { 18 | guard Display.displayType == .iphoneX || force else { return } 19 | 20 | if let statusBarSubstrate = statusBarSubstrate { 21 | statusBarSubstrate.backgroundColor = color 22 | return 23 | } 24 | 25 | let substrate = UIView().add(to: view).then { 26 | $0.topAnchor.constraint(equalTo: view.topAnchor).isActive = true 27 | $0.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true 28 | $0.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true 29 | $0.heightAnchor.constraint(equalToConstant: UIApplication.shared.statusBarFrame.height).isActive = true 30 | $0.backgroundColor = color 31 | $0.layer.zPosition = CGFloat(Float.greatestFiniteMagnitude) 32 | } 33 | statusBarSubstrate = substrate 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-utopia_Example/Pods-utopia_Example-acknowledgements.markdown: -------------------------------------------------------------------------------- 1 | # Acknowledgements 2 | This application makes use of the following third party libraries: 3 | 4 | ## utopia 5 | 6 | Copyright (c) 2018 Dmitriy Kalachev 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in 16 | all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | THE SOFTWARE. 25 | 26 | Generated by CocoaPods - https://cocoapods.org 27 | -------------------------------------------------------------------------------- /utopia/Source/Signals/UIBarButtonItem+Signals.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIBarButtonItem+Signals.swift 3 | // Signals iOS 4 | // 5 | // Created by Linus Unnebäck on 2018-03-09. 6 | // Copyright © 2018 Tuomas Artman. All rights reserved. 7 | // 8 | 9 | #if os(iOS) || os(tvOS) 10 | 11 | import UIKit 12 | 13 | /// Extends UIBarButtonItem with signal for the action. 14 | public extension UIBarButtonItem { 15 | /// A signal that fires for each action event. 16 | var onAction: Signal<(Void)> { 17 | return getOrCreateSignal(); 18 | } 19 | 20 | // MARK: - Private interface 21 | 22 | private struct AssociatedKeys { 23 | static var SignalDictionaryKey = "signals_signalKey" 24 | } 25 | 26 | private func getOrCreateSignal() -> Signal<(Void)> { 27 | let key = "Action" 28 | let dictionary = getOrCreateAssociatedObject(self, associativeKey: &AssociatedKeys.SignalDictionaryKey, defaultValue: NSMutableDictionary(), policy: objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) 29 | 30 | if let signal = dictionary[key] as? Signal<()> { 31 | return signal 32 | } else { 33 | let signal = Signal<()>() 34 | dictionary[key] = signal 35 | self.target = self 36 | self.action = #selector(eventHandlerAction) 37 | return signal 38 | } 39 | } 40 | 41 | @objc private dynamic func eventHandlerAction() { 42 | getOrCreateSignal().fire(()) 43 | } 44 | } 45 | 46 | #endif 47 | -------------------------------------------------------------------------------- /utopia/Source/Utilities/Require.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Require 3 | * Copyright (c) John Sundell 2017 4 | * Licensed under the MIT license. See LICENSE file. 5 | */ 6 | 7 | import Foundation 8 | 9 | public extension Optional { 10 | /** 11 | * Require this optional to contain a non-nil value 12 | * 13 | * This method will either return the value that this optional contains, or trigger 14 | * a `preconditionFailure` with an error message containing debug information. 15 | * 16 | * - parameter hint: Optionally pass a hint that will get included in any error 17 | * message generated in case nil was found. 18 | * 19 | * - return: The value this optional contains. 20 | */ 21 | func require(hint hintExpression: @autoclosure () -> String? = nil, 22 | file: StaticString = #file, 23 | line: UInt = #line) -> Wrapped { 24 | guard let unwrapped = self else { 25 | var message = "Required value was nil in \(file), at line \(line)" 26 | 27 | if let hint = hintExpression() { 28 | message.append(". Debugging hint: \(hint)") 29 | } 30 | 31 | #if !os(Linux) 32 | let exception = NSException( 33 | name: .invalidArgumentException, 34 | reason: message, 35 | userInfo: nil 36 | ) 37 | 38 | exception.raise() 39 | #endif 40 | 41 | preconditionFailure(message) 42 | } 43 | 44 | return unwrapped 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /utopia/Source/Transitions/AlphaTransition.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | public enum CustomTransitionAnimation {} 5 | 6 | extension CustomTransitionAnimation { 7 | 8 | public static func alphaPresent(using transitionContext: UIViewControllerContextTransitioning, duration: TimeInterval) { 9 | 10 | guard let toVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to) else { return } 11 | 12 | let containerView = transitionContext.containerView 13 | 14 | //configure `TO` view controller 15 | let toFrame = transitionContext.finalFrame(for: toVC) 16 | toVC.view.frame = toFrame 17 | containerView.addSubview(toVC.view) 18 | 19 | 20 | toVC.view.alpha = 0 21 | UIView.animate(withDuration: duration, animations: { 22 | toVC.view.alpha = 1 23 | }, completion: { _ in 24 | transitionContext.completeTransition(!transitionContext.transitionWasCancelled) 25 | }) 26 | 27 | } 28 | 29 | public static func alphaDismiss(using transitionContext: UIViewControllerContextTransitioning, duration: TimeInterval, isNavigation: Bool = false, completion: @escaping () -> Void = {}) { 30 | 31 | let fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from) 32 | UIView.animate(withDuration: duration, animations: { 33 | fromVC?.view.alpha = 0 34 | }, completion: { _ in 35 | completion() 36 | transitionContext.completeTransition(!transitionContext.transitionWasCancelled) 37 | }) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /utopia/Source/Transitions/UIView+transitions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | extension UIView { 5 | 6 | public typealias AnimationBlock = (TimeInterval, TimeInterval) -> Void //duration, delay 7 | 8 | private static var customTransitionIdKey: Int = 12 9 | private static var appearanceAnimationKey: Int = 12 10 | private static var disappearanceAnimationKey: Int = 12 11 | 12 | public var customTransitionId: String? { 13 | get { 14 | return objc_getAssociatedObject(self, &UIView.customTransitionIdKey) as? String 15 | } 16 | set { 17 | objc_setAssociatedObject(self, &UIView.customTransitionIdKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_COPY) 18 | } 19 | } 20 | 21 | public var appearanceAnimation: AnimationBlock? { 22 | get { 23 | return objc_getAssociatedObject(self, &UIView.appearanceAnimationKey) as? AnimationBlock 24 | } 25 | set { 26 | objc_setAssociatedObject(self, &UIView.appearanceAnimationKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN) 27 | } 28 | } 29 | 30 | public var disappearanceAnimation: AnimationBlock? { 31 | get { 32 | return objc_getAssociatedObject(self, &UIView.disappearanceAnimationKey) as? AnimationBlock 33 | } 34 | set { 35 | objc_setAssociatedObject(self, &UIView.disappearanceAnimationKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN) 36 | } 37 | } 38 | 39 | public func findView(withTransitionId: String) -> UIView? { 40 | return self.first(where: { $0.customTransitionId == withTransitionId }) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /utopia/Source/UIKit/CollectionLayout/PinnedHeaderFlowLayout.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | public final class PinnedHeaderFlowLayout: UICollectionViewFlowLayout { 5 | 6 | private let header = UICollectionViewLayoutAttributes(forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, with: IndexPath(row: 0, section: 0)) 7 | private var headerSize: CGSize = .zero 8 | 9 | public override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { 10 | return true 11 | } 12 | 13 | public override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { 14 | guard let collectionView = self.collectionView else { return nil } 15 | let attributes = super.layoutAttributesForElements(in: rect) 16 | 17 | let headerAttributes = attributes?.filter { $0.isFirstHeader }.first 18 | if let headerAttributes = headerAttributes { 19 | headerSize = headerAttributes.size 20 | } 21 | 22 | if collectionView.contentOffset.y > 0 { 23 | header.frame.origin.y = collectionView.contentOffset.y 24 | } else { 25 | header.frame.origin.y = 0 26 | } 27 | 28 | var newAttribures = attributes?.filter { !$0.isFirstHeader } 29 | header.frame.size = headerSize 30 | header.zIndex = Int.max 31 | newAttribures?.append(header) 32 | 33 | return newAttribures 34 | } 35 | } 36 | 37 | 38 | private extension UICollectionViewLayoutAttributes { 39 | var isFirstHeader: Bool { 40 | guard let elementKind = representedElementKind else { return false } 41 | return (elementKind == UICollectionView.elementKindSectionHeader && indexPath.section == 0) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /utopia/Source/UIKit/Keyboard/InputVisibilityController/TouchRecognizer.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import UIKit.UIGestureRecognizerSubclass 3 | 4 | open class TouchRecognizer: UIGestureRecognizer { 5 | open var callback: () -> Void 6 | open var ignoreViews: [UIView]? 7 | open var canPreventOtherGestureRecognizers: Bool = true 8 | /// enable touches on view which is firstResponder 9 | open var ignoreFirstResponder: Bool = false 10 | 11 | public init(callback: @escaping () -> Void, ignoreViews views: [UIView]?) { 12 | self.callback = callback 13 | self.ignoreViews = views 14 | 15 | super.init(target: nil, action: nil) 16 | addTarget(self, action: #selector(TouchRecognizer.touchRecongized(_:))) 17 | } 18 | 19 | override init(target: Any?, action: Selector?) { 20 | self.callback = {} 21 | super.init(target: target, action: action) 22 | } 23 | 24 | @objc func touchRecongized(_ sender: UIGestureRecognizer) { 25 | callback() 26 | } 27 | 28 | override open func touchesBegan(_ touches: Set, with event: UIEvent) { 29 | super.touchesBegan(touches, with: event) 30 | 31 | let touch = touches.first! 32 | if ignoreFirstResponder && touch.view?.isFirstResponder == true { 33 | return 34 | } 35 | 36 | for view in (ignoreViews ?? []) { 37 | if view.bounds.contains(touch.location(in: view)) { 38 | self.state = .failed 39 | return 40 | } 41 | } 42 | 43 | self.state = .recognized 44 | } 45 | 46 | open override func canPrevent(_ preventedGestureRecognizer: UIGestureRecognizer) -> Bool { 47 | return canPreventOtherGestureRecognizers 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /utopia/Source/UIKit/CollectionLayout/PaginationFlowLayout.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | public class PaginationFlowLayout: UICollectionViewFlowLayout { 4 | 5 | override public func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint { 6 | 7 | guard let collectionView = collectionView, 8 | let layoutAttributes: Array = layoutAttributesForElements(in: collectionView.bounds), 9 | layoutAttributes.count != 0 else { 10 | return proposedContentOffset 11 | } 12 | 13 | var firstAttribute: UICollectionViewLayoutAttributes = layoutAttributes[0] 14 | for attribute: UICollectionViewLayoutAttributes in layoutAttributes { 15 | guard attribute.representedElementCategory == .cell else { continue } 16 | 17 | switch scrollDirection { 18 | case .horizontal: 19 | if((velocity.x > 0.0 && attribute.center.x > firstAttribute.center.x) || 20 | (velocity.x <= 0.0 && attribute.center.x < firstAttribute.center.x)) { 21 | firstAttribute = attribute; 22 | } 23 | case .vertical: 24 | if((velocity.y > 0.0 && attribute.center.y > firstAttribute.center.y) || 25 | (velocity.y <= 0.0 && attribute.center.y < firstAttribute.center.y)) { 26 | firstAttribute = attribute; 27 | } 28 | @unknown default: 29 | fatalError() 30 | } 31 | } 32 | 33 | switch scrollDirection { 34 | case .horizontal: 35 | return CGPoint(x: firstAttribute.center.x - collectionView.bounds.size.width * 0.5, y: proposedContentOffset.y) 36 | case .vertical: 37 | return CGPoint(x: proposedContentOffset.x, y: firstAttribute.center.y - collectionView.bounds.size.height * 0.5) 38 | @unknown default: 39 | fatalError() 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /utopia/Source/UIKit/Extensions/UIWindow+ext.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | extension UIWindow { 5 | 6 | public func switchRootViewController(_ viewController: UIViewController, onSwitchRVC: (() -> Void)? = nil, completion: (() -> Void)? = nil) { 7 | 8 | guard let previous = rootViewController else { 9 | rootViewController = viewController 10 | completion?() 11 | onSwitchRVC?() 12 | return 13 | } 14 | 15 | let removePreviousController: () -> Void = {[weak previous] in 16 | //mpdal controller have strong reference to it presenting controller, to correct memory management we must dismiss it before replace parent controller. 17 | previous?.dismiss(animated: false, completion: nil) 18 | previous?.view.removeFromSuperview() 19 | previous?.removeFromParent() 20 | } 21 | 22 | guard let snapShot: UIView = subviews.last?.snapshotView() else { 23 | removePreviousController() 24 | delay(TimeInterval.oneFrame) { 25 | self.rootViewController = viewController 26 | onSwitchRVC?() 27 | completion?() 28 | } 29 | return 30 | } 31 | 32 | removePreviousController() 33 | addSubview(snapShot) 34 | 35 | delay(TimeInterval.oneFrame) { //fucking apple! if previous controller is dismissing modal controller it must be in window views hierarchy 36 | snapShot.removeFromSuperview() 37 | self.rootViewController = viewController 38 | 39 | onSwitchRVC?() 40 | 41 | viewController.view.addSubview(snapShot) 42 | UIView.animate(withDuration: 0.6, animations: {() -> Void in 43 | snapShot.layer.opacity = 0 44 | snapShot.layer.transform = CATransform3DMakeScale(1.5, 1.5, 1.5) 45 | }, completion: {(_ finished: Bool) -> Void in 46 | snapShot.removeFromSuperview() 47 | completion?() 48 | }) 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /utopia/Source/Utilities/ScrollDismission.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | 5 | extension UIViewController { 6 | 7 | static let pullToDismissThreshold: CGFloat = 60 8 | 9 | @discardableResult 10 | @objc public dynamic func checkDismission(userPanGesture recognizer: UIPanGestureRecognizer) -> Bool { 11 | let velocity = recognizer.velocity(in: view) 12 | 13 | guard 14 | let scrollView = recognizer.view as? UIScrollView, 15 | recognizer.state == .ended, 16 | (scrollView.contentInset.top + scrollView.contentOffset.y) < -UIViewController.pullToDismissThreshold, 17 | velocity.y > 0 18 | else { return false } 19 | 20 | //disable bounces up scroll view 21 | scrollView.bounces = false 22 | scrollView.contentInset.top = -scrollView.contentOffset.y 23 | scrollView.isUserInteractionEnabled = false 24 | 25 | dismissionCallback?() 26 | dismiss() 27 | return true 28 | } 29 | 30 | public func setupPullToDismiss() { 31 | let recognizer = UIPanGestureRecognizer {[weak self] recognizer in 32 | guard let recognizer = recognizer as? UIPanGestureRecognizer else { return } 33 | let translation = recognizer.translation(in: recognizer.view) 34 | if translation.y > UIViewController.pullToDismissThreshold { 35 | self?.dismiss() 36 | } 37 | } 38 | view.addGestureRecognizer(recognizer) 39 | } 40 | 41 | private static var dismissionCallbackKey: Int = 0 42 | private var dismissionCallback: (() -> Void)? { 43 | get { return objc_getAssociatedObject(self, &UIViewController.dismissionCallbackKey) as? () -> Void } 44 | set { objc_setAssociatedObject(self, &UIViewController.dismissionCallbackKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_COPY) } 45 | } 46 | 47 | public func addPullToDismiss(on scrollView: UIScrollView, dismissionCallback: (() -> Void)? = nil) { 48 | self.dismissionCallback = dismissionCallback 49 | scrollView.panGestureRecognizer.addTarget(self, action: #selector(checkDismission(userPanGesture:))) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /utopia/Source/Validation/ValidatableView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ValidatableView.swift 3 | // utopia 4 | // 5 | // Created by Dmitriy Kalachev on 4/18/18. 6 | // Copyright © 2018 Ramotion. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public protocol ValidationDisplay: class { 12 | var validationResult: ValidationResult { get } 13 | var viewValidationState: ViewValidationState { get set } 14 | func showValidationResult() 15 | } 16 | 17 | public extension Array where Element == ValidationDisplay { 18 | var isAllValid: Bool { 19 | return reduce(true, { $0 && $1.validationResult.isValid }) 20 | } 21 | } 22 | 23 | public protocol ValidatableView: ValidationDisplay { 24 | associatedtype Value 25 | var value: Value { get set } 26 | var valueChanged: (Value) -> Void { get set } 27 | var validator: Validator? { get set } 28 | } 29 | 30 | public extension ValidatableView { 31 | var validationResult: ValidationResult { 32 | return validator?.validate(value) ?? .valid 33 | } 34 | func showValidationResult() { 35 | viewValidationState = ViewValidationState(validationResult) 36 | } 37 | } 38 | 39 | public enum ViewValidationState: Equatable { 40 | case empty 41 | case valid 42 | case invalid(String) 43 | 44 | public init(_ result: ValidationResult) { 45 | switch result { 46 | case .valid: self = .valid 47 | case .invalid(let msg): self = .invalid(msg) 48 | } 49 | } 50 | 51 | public var isInvalid: Bool { 52 | switch self { 53 | case .invalid: return true 54 | default: return false 55 | } 56 | } 57 | 58 | public var isValid: Bool { 59 | switch self { 60 | case .valid: return true 61 | default: return false 62 | } 63 | } 64 | 65 | public var isEmpty: Bool { 66 | switch self { 67 | case .empty: return true 68 | default: return false 69 | } 70 | } 71 | 72 | public var errorMessage: String? { 73 | switch self { 74 | case .invalid(let msg): return msg 75 | case .valid, .empty: return nil 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /utopia/Source/UIKit/Gestures/GestureRecognizerDelegate.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | class GestureRecognizerDelegate: NSObject, UIGestureRecognizerDelegate { 5 | var recognizerShouldBegin: ((UIGestureRecognizer) -> Bool)? = nil 6 | var shouldRecognizeSimultaneously: ((UIGestureRecognizer, UIGestureRecognizer) -> Bool)? = nil 7 | var shouldRequireFailureOf: ((UIGestureRecognizer, UIGestureRecognizer) -> Bool)? = nil 8 | var shouldRequireFailureBy: ((UIGestureRecognizer, UIGestureRecognizer) -> Bool)? = nil 9 | var shouldReceiveTouch: ((UIGestureRecognizer, UITouch) -> Bool)? = nil 10 | var shouldReceivePress: ((UIGestureRecognizer, UIPress) -> Bool)? = nil 11 | 12 | func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { 13 | return recognizerShouldBegin?(gestureRecognizer) ?? true 14 | } 15 | 16 | func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { 17 | return shouldRecognizeSimultaneously?(gestureRecognizer, otherGestureRecognizer) ?? true 18 | } 19 | 20 | func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool { 21 | return shouldRequireFailureOf?(gestureRecognizer, otherGestureRecognizer) ?? true 22 | } 23 | 24 | func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool { 25 | return shouldRequireFailureBy?(gestureRecognizer, otherGestureRecognizer) ?? false 26 | } 27 | 28 | func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { 29 | return shouldReceiveTouch?(gestureRecognizer, touch) ?? true 30 | } 31 | 32 | func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive press: UIPress) -> Bool { 33 | return shouldReceivePress?(gestureRecognizer, press) ?? true 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /utopia/Source/UIKit/BarButtons.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | 5 | extension UIBarButtonItem { 6 | 7 | private class Action { 8 | var action: () -> Void 9 | 10 | init(action: @escaping () -> Void) { 11 | self.action = action 12 | } 13 | 14 | @objc fileprivate func handleAction(_ sender: UIBarButtonItem) { 15 | action() 16 | } 17 | } 18 | 19 | private struct AssociatedKeys { 20 | static var ActionName = "action" 21 | } 22 | 23 | private var barButtonAction: Action? { 24 | set { objc_setAssociatedObject(self, &AssociatedKeys.ActionName, newValue, .OBJC_ASSOCIATION_RETAIN) } 25 | get { return objc_getAssociatedObject(self, &AssociatedKeys.ActionName) as? Action } 26 | } 27 | 28 | public convenience init(image: UIImage?, style: UIBarButtonItem.Style = .plain, action: @escaping () -> Void) { 29 | let handler = Action(action: action) 30 | self.init(image: image, style: style, target: handler, action: #selector(Action.handleAction(_:))) 31 | barButtonAction = handler 32 | } 33 | 34 | public convenience init(title: String?, style: UIBarButtonItem.Style = .plain, action: @escaping () -> Void) { 35 | let handler = Action(action: action) 36 | self.init(title: title, style: style, target: handler, action: #selector(Action.handleAction(_:))) 37 | barButtonAction = handler 38 | } 39 | 40 | public convenience init(barButtonSystemItem: UIBarButtonItem.SystemItem, action: @escaping () -> Void) { 41 | let handler = Action(action: action) 42 | self.init(barButtonSystemItem: barButtonSystemItem, target: handler, action: #selector(Action.handleAction(_:))) 43 | barButtonAction = handler 44 | } 45 | 46 | public convenience init(image: UIImage?, landscapeImagePhone: UIImage?, style: UIBarButtonItem.Style, action: @escaping () -> Void) { 47 | let handler = Action(action: action) 48 | self.init(image: image, landscapeImagePhone: landscapeImagePhone, style: style, target: handler, action: #selector(Action.handleAction(_:))) 49 | barButtonAction = handler 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /utopia/Source/Utilities/AsyncOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AsyncOperation.swift 3 | // Radiant Tap Essentials 4 | // 5 | // Copyright © 2017 Radiant Tap 6 | // MIT License · http://choosealicense.com/licenses/mit/ 7 | // 8 | 9 | import Foundation 10 | 11 | open class AsyncOperation : Operation { 12 | public enum State { 13 | case ready 14 | case executing 15 | case finished 16 | 17 | fileprivate var key: String { 18 | switch self { 19 | case .ready: 20 | return "isReady" 21 | case .executing: 22 | return "isExecuting" 23 | case .finished: 24 | return "isFinished" 25 | } 26 | } 27 | } 28 | 29 | fileprivate(set) public var state = State.ready { 30 | willSet { 31 | willChangeValue(forKey: state.key) 32 | willChangeValue(forKey: newValue.key) 33 | } 34 | didSet { 35 | didChangeValue(forKey: oldValue.key) 36 | didChangeValue(forKey: state.key) 37 | } 38 | } 39 | 40 | final override public var isAsynchronous: Bool { 41 | return true 42 | } 43 | 44 | final override public var isExecuting: Bool { 45 | return state == .executing 46 | } 47 | 48 | final override public var isFinished: Bool { 49 | return state == .finished 50 | } 51 | 52 | final override public var isReady: Bool { 53 | return state == .ready && super.isReady 54 | } 55 | 56 | 57 | //MARK: Setup 58 | 59 | /// Do not override this method, ever. Call it from `workItem()` instead 60 | final public func markFinished() { 61 | if state != .finished { 62 | state = .finished 63 | } 64 | } 65 | 66 | /// You **should** override this method and start and/or do your async work here. 67 | /// **Must** call `markFinished()` inside your override 68 | /// when async work is done since operation needs to be mark `finished`. 69 | open func workItem() { 70 | markFinished() 71 | } 72 | 73 | required override public init() { 74 | } 75 | 76 | //MARK: Control 77 | 78 | final override public func start() { 79 | if isCancelled { 80 | state = .finished 81 | return 82 | } 83 | 84 | main() 85 | } 86 | 87 | final override public func main() { 88 | if isCancelled { 89 | state = .finished 90 | return 91 | } 92 | 93 | state = .executing 94 | workItem() 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /utopia/Source/UIKit/Extensions/UIView+ext.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | public extension UIView { 4 | 5 | @discardableResult 6 | func add(to superview: UIView) -> Self { 7 | superview.addSubview(self) 8 | return self 9 | } 10 | 11 | @discardableResult 12 | func insert(to superview: UIView, at index: Int) -> Self { 13 | superview.insertSubview(self, at: index) 14 | return self 15 | } 16 | 17 | @discardableResult 18 | func insert(to superview: UIView, above view: UIView) -> Self { 19 | superview.insertSubview(self, aboveSubview: view) 20 | return self 21 | } 22 | 23 | @discardableResult 24 | func insert(to superview: UIView, below view: UIView) -> Self { 25 | superview.insertSubview(self, belowSubview: view) 26 | return self 27 | } 28 | 29 | @discardableResult 30 | func add(to stackview: UIStackView) -> Self { 31 | stackview.addArrangedSubview(self) 32 | return self 33 | } 34 | 35 | convenience init(size: CGSize) { 36 | self.init(frame: CGRect(origin: CGPoint.zero, size: size)) 37 | } 38 | } 39 | 40 | 41 | extension UIView { 42 | 43 | public func removeAllConstraintsDownInHierarchy () { 44 | self.removeSelfConstraints() 45 | let subviews: [UIView] = findAll() 46 | subviews.forEach { $0.removeSelfConstraints() } 47 | } 48 | 49 | public func removeSelfConstraints () { 50 | var superview = self.superview 51 | while superview != nil { 52 | for c in superview!.constraints { 53 | if c.firstItem as! NSObject == self || c.secondItem as! NSObject == self { 54 | superview?.removeConstraint(c) 55 | } 56 | } 57 | superview = superview?.superview 58 | } 59 | self.removeConstraints(self.constraints) 60 | self.translatesAutoresizingMaskIntoConstraints = true 61 | } 62 | } 63 | 64 | extension UIView { 65 | 66 | public func pauseLayerAnimations() { 67 | let pausedTime = layer.convertTime(CACurrentMediaTime(), from: nil) 68 | layer.speed = 0.0 69 | layer.timeOffset = pausedTime 70 | } 71 | 72 | public func resumeLayerAnimations() { 73 | let pausedTime = layer.timeOffset 74 | layer.speed = 1.0 75 | layer.timeOffset = 0.0 76 | layer.beginTime = 0.0 77 | let timeSincePause = layer.convertTime(CACurrentMediaTime(), from: nil) - pausedTime 78 | layer.beginTime = timeSincePause 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-utopia_Example/Pods-utopia_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 (c) 2018 Dmitriy Kalachev <dima.kalachov@gmail.com> 18 | 19 | Permission is hereby granted, free of charge, to any person obtaining a copy 20 | of this software and associated documentation files (the "Software"), to deal 21 | in the Software without restriction, including without limitation the rights 22 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 23 | copies of the Software, and to permit persons to whom the Software is 24 | furnished to do so, subject to the following conditions: 25 | 26 | The above copyright notice and this permission notice shall be included in 27 | all copies or substantial portions of the Software. 28 | 29 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 30 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 31 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 32 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 33 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 34 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 35 | THE SOFTWARE. 36 | 37 | License 38 | MIT 39 | Title 40 | utopia 41 | Type 42 | PSGroupSpecifier 43 | 44 | 45 | FooterText 46 | Generated by CocoaPods - https://cocoapods.org 47 | Title 48 | 49 | Type 50 | PSGroupSpecifier 51 | 52 | 53 | StringsTable 54 | Acknowledgements 55 | Title 56 | Acknowledgements 57 | 58 | 59 | -------------------------------------------------------------------------------- /utopia/Source/UIKit/LayoutGuides.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | 5 | extension UIView { 6 | 7 | public var safeLayoutGuide: UILayoutGuide { 8 | if #available(iOS 11.0, *) { 9 | return safeAreaLayoutGuide 10 | } else { 11 | return layoutMarginsGuide 12 | } 13 | } 14 | 15 | public var safeTopAnchor: NSLayoutYAxisAnchor { 16 | if #available(iOS 11.0, *) { 17 | return safeAreaLayoutGuide.topAnchor 18 | } else { 19 | return topAnchor 20 | } 21 | } 22 | 23 | public var safeBottomAnchor: NSLayoutYAxisAnchor { 24 | if #available(iOS 11.0, *) { 25 | return safeAreaLayoutGuide.bottomAnchor 26 | } else { 27 | return bottomAnchor 28 | } 29 | } 30 | 31 | public var safeRightAnchor: NSLayoutXAxisAnchor { 32 | if #available(iOS 11.0, *) { 33 | return safeAreaLayoutGuide.rightAnchor 34 | } else { 35 | return rightAnchor 36 | } 37 | } 38 | 39 | public var safeLeftAnchor: NSLayoutXAxisAnchor { 40 | if #available(iOS 11.0, *) { 41 | return safeAreaLayoutGuide.leftAnchor 42 | } else { 43 | return leftAnchor 44 | } 45 | } 46 | 47 | public var safeLeadingAnchor: NSLayoutXAxisAnchor { 48 | if #available(iOS 11.0, *) { 49 | return safeAreaLayoutGuide.leadingAnchor 50 | } else { 51 | return leadingAnchor 52 | } 53 | } 54 | 55 | public var safeTrailingAnchor: NSLayoutXAxisAnchor { 56 | if #available(iOS 11.0, *) { 57 | return safeAreaLayoutGuide.trailingAnchor 58 | } else { 59 | return trailingAnchor 60 | } 61 | } 62 | 63 | public var safeCenterXAnchor: NSLayoutXAxisAnchor { 64 | if #available(iOS 11.0, *) { 65 | return safeAreaLayoutGuide.centerXAnchor 66 | } else { 67 | return centerXAnchor 68 | } 69 | } 70 | 71 | public var safeCenterYAnchor: NSLayoutYAxisAnchor { 72 | if #available(iOS 11.0, *) { 73 | return safeAreaLayoutGuide.centerYAnchor 74 | } else { 75 | return centerYAnchor 76 | } 77 | } 78 | 79 | public var safeHeightAnchor: NSLayoutDimension { 80 | if #available(iOS 11.0, *) { 81 | return safeAreaLayoutGuide.heightAnchor 82 | } else { 83 | return heightAnchor 84 | } 85 | } 86 | 87 | public var safeWidthAnchor: NSLayoutDimension { 88 | if #available(iOS 11.0, *) { 89 | return safeAreaLayoutGuide.widthAnchor 90 | } else { 91 | return widthAnchor 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /utopia/Source/UIKit/Animations/TouchInteraction.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | import UIKit.UIGestureRecognizerSubclass 4 | 5 | 6 | protocol TouchInteraction: class { 7 | func setupDefaultTouchInteraction() 8 | } 9 | 10 | 11 | extension UIView: TouchInteraction {} 12 | 13 | 14 | extension TouchInteraction where Self: UIView { 15 | 16 | func setupDefaultTouchInteraction() { 17 | 18 | let recognizer = TouchInteractionRecognizer { recognizer in 19 | switch recognizer.state { 20 | case .began: 21 | UIView.animate(withDuration: 0.1, delay: 0, options: [.beginFromCurrentState, .allowUserInteraction], animations: { 22 | self.transform = CGAffineTransform(scaleX: 0.9, y: 0.9) 23 | self.alpha = 0.6 24 | }) 25 | case .ended, .failed, .cancelled: 26 | UIView.animate(withDuration: 0.175, delay: 0, options: [.beginFromCurrentState, .allowUserInteraction], animations: { 27 | self.transform = CGAffineTransform(scaleX: 1, y: 1) 28 | self.alpha = 1 29 | }) 30 | default: break 31 | } 32 | } 33 | 34 | self.addGestureRecognizer(recognizer) 35 | self.isUserInteractionEnabled = true 36 | } 37 | } 38 | 39 | 40 | class TouchInteractionRecognizer: UIGestureRecognizer { 41 | 42 | override init(target: Any?, action: Selector?) { 43 | super.init(target: target, action: action) 44 | delaysTouchesBegan = false 45 | delaysTouchesEnded = false 46 | cancelsTouchesInView = false 47 | } 48 | 49 | override func touchesBegan(_ touches: Set, with event: UIEvent) { 50 | super.touchesBegan(touches, with: event) 51 | if touches.count != 1 { 52 | state = .failed 53 | } 54 | state = .began 55 | } 56 | 57 | override func touchesEnded(_ touches: Set, with event: UIEvent) { 58 | super.touchesEnded(touches, with: event) 59 | if touches.count != 1 { 60 | state = .failed 61 | } 62 | state = .ended 63 | } 64 | 65 | override func touchesCancelled(_ touches: Set, with event: UIEvent) { 66 | super.touchesCancelled(touches, with: event) 67 | state = .cancelled 68 | } 69 | 70 | override func reset() { 71 | super.reset() 72 | state = .possible 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /utopia/Source/UIKit/Extensions/UIGestureRecognizer+ext.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | extension UIGestureRecognizer { 5 | private class GestureAction { 6 | var action: (UIGestureRecognizer) -> Void 7 | 8 | init(action: @escaping (UIGestureRecognizer) -> Void) { 9 | self.action = action 10 | } 11 | } 12 | 13 | private struct AssociatedKeys { 14 | static var ActionName = "action" 15 | } 16 | 17 | private var gestureAction: GestureAction? { 18 | set { objc_setAssociatedObject(self, &AssociatedKeys.ActionName, newValue, .OBJC_ASSOCIATION_RETAIN) } 19 | get { return objc_getAssociatedObject(self, &AssociatedKeys.ActionName) as? GestureAction } 20 | } 21 | 22 | /** 23 | Convenience initializer, associating an action closure with the gesture recognizer (instead of the more traditional target/action). 24 | 25 | - parameter action: The closure for the recognizer to execute. There is no pre-logic to conditionally invoke the closure or not (e.g. only invoke the closure if the gesture recognizer is in a particular state). The closure is merely invoked directly; all handler logic is up to the closure. 26 | 27 | - returns: The UIGestureRecognizer. 28 | */ 29 | public convenience init(action: @escaping (UIGestureRecognizer) -> Void) { 30 | self.init() 31 | gestureAction = GestureAction(action: action) 32 | addTarget(self, action: #selector(handleAction(_:))) 33 | } 34 | 35 | @objc dynamic private func handleAction(_ recognizer: UIGestureRecognizer) { 36 | gestureAction?.action(recognizer) 37 | } 38 | } 39 | 40 | extension UIView { 41 | 42 | public enum GestureType { 43 | case tap 44 | case longPress 45 | case pan 46 | case swipe(UISwipeGestureRecognizer.Direction) 47 | case tapCount(Int) 48 | } 49 | 50 | public func addGesture(type: GestureType, callback: @escaping (UIGestureRecognizer) -> Void) { 51 | switch type { 52 | case .tap: 53 | addGestureRecognizer(UITapGestureRecognizer(action: callback)) 54 | case .tapCount(let count): 55 | addGestureRecognizer(UITapGestureRecognizer(action: callback).then { $0.numberOfTapsRequired = count }) 56 | case .longPress: 57 | addGestureRecognizer(UILongPressGestureRecognizer(action: callback)) 58 | case .pan: 59 | addGestureRecognizer(UIPanGestureRecognizer(action: callback)) 60 | case .swipe(let direction): 61 | addGestureRecognizer(UISwipeGestureRecognizer(action: callback).then { $0.direction = direction }) 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /utopia/Source/UIKit/CollectionLayout/SnappingFlowLayout.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | 5 | final class SnappingFlowLayout: UICollectionViewFlowLayout { 6 | 7 | override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint { 8 | 9 | guard let collectionView = collectionView else { 10 | return super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity) 11 | } 12 | 13 | var offsetAdjustment = CGFloat.greatestFiniteMagnitude 14 | switch scrollDirection { 15 | case .horizontal: 16 | let horizontalOffset = proposedContentOffset.x + collectionView.contentInset.left 17 | 18 | let targetRect = CGRect(x: proposedContentOffset.x, y: 0, width: collectionView.bounds.size.width, height: collectionView.bounds.size.height) 19 | 20 | let layoutAttributesArray = super.layoutAttributesForElements(in: targetRect) 21 | 22 | layoutAttributesArray?.forEach { layoutAttributes in 23 | let itemOffset = layoutAttributes.frame.origin.x 24 | if fabsf(Float(itemOffset - horizontalOffset)) < fabsf(Float(offsetAdjustment)) { 25 | offsetAdjustment = itemOffset - horizontalOffset 26 | } 27 | } 28 | let result = CGPoint(x: proposedContentOffset.x + offsetAdjustment, y: proposedContentOffset.y) 29 | return result 30 | 31 | case .vertical: 32 | let verticalOffset = proposedContentOffset.y + collectionView.contentInset.top 33 | 34 | let targetRect = CGRect(x: 0, y: proposedContentOffset.y, width: collectionView.bounds.size.width, height: collectionView.bounds.size.height) 35 | 36 | let layoutAttributesArray = super.layoutAttributesForElements(in: targetRect) 37 | layoutAttributesArray?.forEach { layoutAttributes in 38 | let itemOffset = layoutAttributes.frame.origin.y 39 | if fabsf(Float(itemOffset - verticalOffset)) < fabsf(Float(offsetAdjustment)) { 40 | offsetAdjustment = itemOffset - verticalOffset 41 | } 42 | } 43 | 44 | let result = CGPoint(x: proposedContentOffset.x, y: proposedContentOffset.y + offsetAdjustment) 45 | return result 46 | @unknown default: 47 | fatalError() 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /utopia/Source/UIKit/Extensions/UIAlertController+ext.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | extension UIAlertController { 5 | 6 | /** 7 | Adds a UIAlertAction, initialized with a title, style, and completion handler. 8 | 9 | - parameter title: The text to use for the action title. 10 | - parameter style: The style to use for the action (optional, defaults to `.default`). 11 | - parameter handler: A closure to execute when the user selects the action (optional, defaults to `nil`). 12 | 13 | - returns: self 14 | */ 15 | @discardableResult 16 | public func addAction(title: String?, style: UIAlertAction.Style = .default, handler: ((UIAlertAction) -> Void)? = nil) -> Self { 17 | addAction(UIAlertAction(title: title, style: style, handler: handler)) 18 | return self 19 | } 20 | 21 | /** 22 | Creates a UIAlertController instance, styled as an action sheet. 23 | 24 | - parameter title: The title for the controller (optional, defaults to `nil`). 25 | - parameter message: The message for the controller (optional, defaults to `nil`). 26 | 27 | - returns: A UIAlertController instance, styled as an action sheet. 28 | */ 29 | public static func actionSheet(title: String? = nil, message: String? = nil) -> UIAlertController { 30 | return UIAlertController(title: title, message: message, preferredStyle: .actionSheet) 31 | } 32 | 33 | /** 34 | Creates a UIAlertController instance, styled as an alert. 35 | 36 | - parameter title: The title for the controller (optional, defaults to `nil`). 37 | - parameter message: The message for the controller (optional, defaults to `nil`). 38 | 39 | - returns: A UIAlertController instance, styled as an alert. 40 | */ 41 | public static func alert(title: String? = nil, message: String? = nil) -> UIAlertController { 42 | return UIAlertController(title: title, message: message, preferredStyle: .alert) 43 | } 44 | 45 | /** 46 | Presents the UIAlertController inside the specified view controller. 47 | 48 | - parameter controller: The controller in which to present the alert controller (optional, defaults to `.current`). 49 | - parameter animated: Pass `true` to animate the presentation; otherwise, pass `false` (optional, defaults to `true`). 50 | - parameter completion: The closure to execute after the presentation finishes (optional, defaults to `nil`). 51 | */ 52 | public func present(in controller: UIViewController? = .current, animated: Bool = true, completion: (() -> Void)? = nil) { 53 | controller?.present(self, animated: animated, completion: completion) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /utopia/Source/SwiftStdLib+ext/Random.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import CoreGraphics 3 | 4 | public extension Int { 5 | 6 | /// Returns a random Int point number between 0 and Int.max. 7 | static var random: Int { 8 | return Int.random(n: Int.max) 9 | } 10 | 11 | /// Random integer between 0 and n-1. 12 | /// 13 | /// - Parameter n: Interval max 14 | /// - Returns: Returns a random Int point number between 0 and n max 15 | static func random(n: Int) -> Int { 16 | return Int(arc4random_uniform(UInt32(n))) 17 | } 18 | 19 | /// Random integer between min and max 20 | /// 21 | /// - Parameters: 22 | /// - min: Interval minimun 23 | /// - max: Interval max 24 | /// - Returns: Returns a random Int point number between 0 and n max 25 | static func random(min: Int, max: Int) -> Int { 26 | return Int.random(n: max - min + 1) + min 27 | } 28 | } 29 | 30 | // MARK: Double Extension 31 | 32 | public extension Double { 33 | 34 | /// Returns a random floating point number between 0.0 and 1.0, inclusive. 35 | static var random: Double { 36 | return Double(arc4random()) / 0xFFFFFFFF 37 | } 38 | 39 | /// Random double between 0 and n-1. 40 | /// 41 | /// - Parameter n: Interval max 42 | /// - Returns: Returns a random double point number between 0 and n max 43 | static func random(min: Double, max: Double) -> Double { 44 | return Double.random * (max - min) + min 45 | } 46 | } 47 | 48 | // MARK: Float Extension 49 | 50 | public extension Float { 51 | 52 | /// Returns a random floating point number between 0.0 and 1.0, inclusive. 53 | static var random: Float { 54 | return Float(arc4random()) / 4294967296 55 | } 56 | 57 | /// Random float between 0 and n-1. 58 | /// 59 | /// - Parameter n: Interval max 60 | /// - Returns: Returns a random float point number between 0 and n max 61 | static func random(min: Float, max: Float) -> Float { 62 | return Float.random * (max - min) + min 63 | } 64 | } 65 | 66 | // MARK: CGFloat Extension 67 | 68 | public extension CGFloat { 69 | 70 | /// Randomly returns either 1.0 or -1.0. 71 | static var randomSign: CGFloat { 72 | return (arc4random_uniform(2) == 0) ? 1.0 : -1.0 73 | } 74 | 75 | /// Returns a random floating point number between 0.0 and 1.0, inclusive. 76 | static var random: CGFloat { 77 | return CGFloat(Float.random) 78 | } 79 | 80 | /// Random CGFloat between 0 and n-1. 81 | /// 82 | /// - Parameter n: Interval max 83 | /// - Returns: Returns a random CGFloat point number between 0 and n max 84 | static func random(min: CGFloat, max: CGFloat) -> CGFloat { 85 | return CGFloat.random * (max - min) + min 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /utopia/Source/SwiftStdLib+ext/Collections+ext.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Collections+ext.swift 3 | // Vendefy 4 | // 5 | // Created by Dmitriy Kalachev on 4/11/18. 6 | // Copyright © 2018 Ramotion. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public extension Collection { 12 | var isNotEmpty: Bool { 13 | return !isEmpty 14 | } 15 | } 16 | 17 | public extension Dictionary { 18 | func mapKeys(_ transform: (Key) -> T) -> Dictionary { 19 | var result = Dictionary() 20 | result.reserveCapacity(count) 21 | self.forEach { key, value in 22 | result[transform(key)] = value 23 | } 24 | return result 25 | } 26 | 27 | func merging(_ other: [Key: Value]) -> [Key: Value] { 28 | return merging(other, uniquingKeysWith: { l,r in r }) 29 | } 30 | } 31 | 32 | public extension Optional where Wrapped: Collection { 33 | var isNilOrEmpty: Bool { 34 | switch self { 35 | case let collection?: 36 | return collection.isEmpty 37 | case nil: 38 | return true 39 | } 40 | } 41 | } 42 | 43 | extension Array { 44 | func chunk(_ chunkSize: Int) -> [[Element]] { 45 | return stride(from: 0, to: self.count, by: chunkSize).map({ (startIndex) -> [Element] in 46 | let endIndex = (startIndex.advanced(by: chunkSize) > self.count) ? self.count-startIndex : chunkSize 47 | return Array(self[startIndex.. Self.Iterator.Element? { 61 | return at(i) 62 | } 63 | 64 | /** 65 | Returns an optional element. If the `index` does not exist in the collection, the function returns nil. 66 | 67 | - parameter index: The index of the element to return, if it exists. 68 | 69 | - returns: An optional element from the collection at the specified index. 70 | */ 71 | public func at(_ i: Index) -> Self.Iterator.Element? { 72 | return indices.contains(i) ? self[i] : nil 73 | } 74 | } 75 | 76 | 77 | public extension MutableCollection where Index == Int { 78 | 79 | /** 80 | Returns a random element from the collection. 81 | - returns: A random element from the collection. 82 | */ 83 | func random() -> Iterator.Element { 84 | let index = Int(arc4random_uniform(UInt32(count))) 85 | return self[index] 86 | } 87 | } 88 | 89 | extension Array { 90 | public mutating func removeObject(_ obj: T) { 91 | self = self.filter({ $0 as? T != obj }) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /utopia/Source/Utilities/DateUtilities.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct DateUtilities { 4 | 5 | public static func agoFormat(_ date: Date) -> String? { 6 | let dateFormatter = DateComponentsFormatter() 7 | dateFormatter.unitsStyle = DateComponentsFormatter.UnitsStyle.abbreviated 8 | dateFormatter.allowedUnits = [.second, .minute, .year, .month, .day, .hour] 9 | dateFormatter.zeroFormattingBehavior = DateComponentsFormatter.ZeroFormattingBehavior.dropAll 10 | let rerult = dateFormatter.string(from: date, to: Date()) 11 | return rerult 12 | } 13 | 14 | public static func postedDateFormat(fromDate: Date, toDate: Date) -> String { 15 | let calendar = Calendar.current 16 | let flags: Set = [.second, .minute, .hour, .day, .month, .year] 17 | let components: DateComponents = calendar.dateComponents(flags, from: fromDate, to: toDate) 18 | 19 | func checkCalendarComponent(_ component: Int?, _ suffix: String) -> String? { 20 | guard let dateComponent = component, dateComponent != 0, dateComponent != 0 else { return nil } 21 | return String(dateComponent) + suffix 22 | } 23 | 24 | if let ago = checkCalendarComponent(components.year, "y") { return ago } 25 | if let ago = checkCalendarComponent(components.month, "mo") { return ago } 26 | if let ago = checkCalendarComponent(components.day, "d") { return ago } 27 | if let ago = checkCalendarComponent(components.hour, "h") { return ago } 28 | if let ago = checkCalendarComponent(components.minute, "m") { return ago } 29 | if let ago = checkCalendarComponent(components.second, "s") { return ago } 30 | return "1s" 31 | } 32 | 33 | fileprivate static let durationDateFormatter: DateFormatter = DateUtilities.mkDurationDateFormatter() 34 | fileprivate static func mkDurationDateFormatter() -> DateFormatter { 35 | let durationDateFormatter = DateFormatter() 36 | durationDateFormatter.timeZone = TimeZone(secondsFromGMT: 0) 37 | durationDateFormatter.dateFormat = DateFormatter.dateFormat(fromTemplate: "HH:mm:ss", options: 0, locale: nil) 38 | 39 | return durationDateFormatter 40 | } 41 | 42 | static public func calendarDateString(_ date: Date) -> String { 43 | let dateFormater = DateFormatter() 44 | dateFormater.timeZone = TimeZone(secondsFromGMT: 0) 45 | dateFormater.dateFormat = "dd MM, yyyy" 46 | let result = dateFormater.string(from: date) 47 | return result 48 | } 49 | 50 | static public func getAppDateInstallation() -> Date? { 51 | do { 52 | let bundleRoot = Bundle.main.bundlePath // e.g. /var/mobile/Applications//.app 53 | let manager = FileManager.default 54 | let attrs = try manager.attributesOfItem(atPath: bundleRoot) 55 | return attrs[FileAttributeKey.creationDate] as? Date 56 | } catch { 57 | return nil 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /utopia/Source/UIKit/Extensions/UICollectionView+Reusable.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | public extension UICollectionView { 4 | 5 | func registerCell( 6 | _ type: T.Type, 7 | withIdentifier reuseIdentifier: String = T.reuseIdentifier) 8 | { 9 | register(T.self, forCellWithReuseIdentifier: reuseIdentifier) 10 | } 11 | 12 | func dequeueCell( 13 | _ type: T.Type = T.self, 14 | withIdentifier reuseIdentifier: String = T.reuseIdentifier, 15 | for indexPath: IndexPath) -> T 16 | { 17 | guard let cell = dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as? T else { 18 | fatalError("Unknown cell type (\(T.self)) for reuse identifier: \(reuseIdentifier)") 19 | } 20 | return cell 21 | } 22 | 23 | func registerSupplement( 24 | _ type: T.Type, 25 | kind: String, 26 | withIdentifier reuseIdentifier: String = T.reuseIdentifier) 27 | { 28 | register(T.self, forSupplementaryViewOfKind: kind, withReuseIdentifier: reuseIdentifier) 29 | } 30 | 31 | func dequeueSupplement( 32 | _ type: T.Type = T.self, 33 | kind: String, 34 | withIdentifier reuseIdentifier: String = T.reuseIdentifier, 35 | for indexPath: IndexPath) -> T 36 | { 37 | guard let view = dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: reuseIdentifier, for: indexPath) as? T else { 38 | fatalError("Unknown \(kind) type (\(T.self)) for reuse identifier: \(reuseIdentifier)") 39 | } 40 | return view 41 | } 42 | 43 | func registerHeader( 44 | _ type: T.Type, 45 | withIdentifier reuseIdentifier: String = T.reuseIdentifier) 46 | { 47 | registerSupplement(T.self, kind: UICollectionView.elementKindSectionHeader) 48 | } 49 | 50 | func dequeueHeader( 51 | _ type: T.Type = T.self, 52 | withIdentifier reuseIdentifier: String = T.reuseIdentifier, 53 | for indexPath: IndexPath) -> T 54 | { 55 | return dequeueSupplement(kind: UICollectionView.elementKindSectionHeader, for: indexPath) 56 | } 57 | 58 | func registerFooter( 59 | _ type: T.Type, 60 | withIdentifier reuseIdentifier: String = T.reuseIdentifier) 61 | { 62 | registerSupplement(T.self, kind: UICollectionView.elementKindSectionFooter) 63 | } 64 | 65 | func dequeueFooter( 66 | _ type: T.Type = T.self, 67 | withIdentifier reuseIdentifier: String = T.reuseIdentifier, 68 | for indexPath: IndexPath) -> T 69 | { 70 | return dequeueSupplement(kind: UICollectionView.elementKindSectionFooter, for: indexPath) 71 | } 72 | 73 | } 74 | 75 | public extension UICollectionReusableView { 76 | 77 | static var reuseIdentifier: String { 78 | return String(describing: self) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /utopia/Source/UIKit/Extensions/UIViewController+ext.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UIViewController { 4 | 5 | /// Retrieve the view controller currently on-screen 6 | /// 7 | /// Based off code here: http://stackoverflow.com/questions/24825123/get-the-current-view-controller-from-the-app-delegate 8 | public static var current: UIViewController? { 9 | if let controller = UIApplication.shared.keyWindow?.rootViewController { 10 | return findCurrent(controller) 11 | } 12 | return nil 13 | } 14 | 15 | private static func findCurrent(_ controller: UIViewController) -> UIViewController { 16 | if let controller = controller.presentedViewController { 17 | return findCurrent(controller) 18 | } else if let controller = controller as? UISplitViewController, let lastViewController = controller.viewControllers.first, controller.viewControllers.count > 0 { 19 | return findCurrent(lastViewController) 20 | } else if let controller = controller as? UINavigationController, let topViewController = controller.topViewController, controller.viewControllers.count > 0 { 21 | return findCurrent(topViewController) 22 | } else if let controller = controller as? UITabBarController, let selectedViewController = controller.selectedViewController, (controller.viewControllers?.count ?? 0) > 0 { 23 | return findCurrent(selectedViewController) 24 | } else { 25 | return controller 26 | } 27 | } 28 | } 29 | 30 | extension UIViewController { 31 | 32 | // SwifterSwift: Check if ViewController is onscreen and not hidden. from https://github.com/SwifterSwift/SwifterSwift 33 | public var isVisible: Bool { 34 | // http://stackoverflow.com/questions/2777438/how-to-tell-if-uiviewcontrollers-view-is-visible 35 | return self.isViewLoaded && view.window != nil 36 | } 37 | 38 | public func dismiss() { 39 | if let navigationController = navigationController, navigationController.viewControllers.count > 1 { 40 | navigationController.popViewController(animated: true) 41 | } 42 | else { 43 | dismiss(animated: true) 44 | } 45 | } 46 | 47 | public func dismissToRootViewController(animated: Bool) { 48 | var vc = self 49 | while let current = vc.presentingViewController { 50 | vc = current 51 | } 52 | vc.dismiss(animated: animated, completion: nil) 53 | vc.navigationController?.popToRootViewController(animated: animated) 54 | } 55 | } 56 | 57 | extension UIViewController { 58 | public func add(_ child: UIViewController) { 59 | addChild(child) 60 | view.addSubview(child.view) 61 | child.didMove(toParent: self) 62 | } 63 | 64 | public func remove() { 65 | guard parent != nil else { 66 | return 67 | } 68 | 69 | willMove(toParent: nil) 70 | removeFromParent() 71 | view.removeFromSuperview() 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /utopia/Source/UIKit/Display.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | public struct DisplaySize { 5 | public static let iphone4 = CGSize(width: 320, height: 480) 6 | public static let iphone5 = CGSize(width: 320, height: 568) 7 | public static let iphone6 = CGSize(width: 375, height: 667) 8 | public static let iphone6plus = CGSize(width: 414, height: 736) 9 | public static let iphoneX = CGSize(width: 375, height: 812) 10 | public static let iphoneXR = CGSize(width: 414, height: 896) 11 | public static let iPad9 = CGSize(width: 768, height: 1024) 12 | public static let ipad10 = CGSize(width: 834, height: 1112) 13 | public static let ipad12 = CGSize(width: 1024, height: 1366) 14 | public static let unknown = CGSize.zero 15 | } 16 | 17 | public enum DisplayType { 18 | case unknown 19 | case iphone4 20 | case iphone5 21 | case iphone6 22 | case iphone6plus 23 | case iphoneX 24 | case iphoneXR 25 | case iphoneXM 26 | case ipad9 27 | case ipad10 28 | case ipad12 29 | } 30 | 31 | public enum Display { 32 | public static var width : CGFloat { return UIScreen.main.bounds.size.width } 33 | public static var height : CGFloat { return UIScreen.main.bounds.size.height } 34 | public static var maxLength : CGFloat { return max(width, height) } 35 | public static var minLength : CGFloat { return min(width, height) } 36 | public static var zoomed : Bool { return UIScreen.main.nativeScale >= UIScreen.main.scale } 37 | public static var retina : Bool { return UIScreen.main.scale >= 2.0 } 38 | public static var phone : Bool { return UIDevice.current.userInterfaceIdiom == .phone } 39 | public static var pad : Bool { return UIDevice.current.userInterfaceIdiom == .pad } 40 | 41 | public static var navbarSize : CGFloat { 42 | return (Display.height == 812 || Display.height == 896) ? 88 : 64 43 | } 44 | public static var bottombarSize : CGFloat { 45 | return (Display.height == 812 || Display.height == 896) ? 34 : 0 46 | } 47 | 48 | public static var displayType: DisplayType { 49 | if phone && maxLength < 568 { 50 | return .iphone4 51 | } else if phone && maxLength == 568 { 52 | return .iphone5 53 | } else if phone && maxLength == 667 { 54 | return .iphone6 55 | } else if phone && maxLength == 736 { 56 | return .iphone6plus 57 | } else if phone && maxLength == 896 { 58 | return .iphoneXR 59 | } else if phone && maxLength == 812 { 60 | return .iphoneX 61 | } else if pad && maxLength == 1024 { 62 | return .ipad9 63 | } else if pad && maxLength == 1112 { 64 | return .ipad10 65 | } else if pad && maxLength == 1366 { 66 | return .ipad12 67 | } 68 | return .unknown 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /utopia/Source/Utilities/Math.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | public enum Math { 5 | public static func lerp(from: T, to: T, progress: T) -> T { 6 | return from + progress * (to - from); 7 | } 8 | 9 | public static func lerp(from: UIColor, to: UIColor, progress: CGFloat) -> UIColor { 10 | let rgba1 = from.rgba 11 | let rgba2 = to.rgba 12 | let r = lerp(from: rgba1.r, to: rgba2.r, progress: progress) 13 | let g = lerp(from: rgba1.g, to: rgba2.g, progress: progress) 14 | let b = lerp(from: rgba1.b, to: rgba2.b, progress: progress) 15 | let a = lerp(from: rgba1.a, to: rgba2.a, progress: progress) 16 | return UIColor(red: r, green: g, blue: b, alpha: a) 17 | } 18 | 19 | public static func lerp(from: CGRect, to: CGRect, progress: CGFloat) -> CGRect { 20 | let x = lerp(from: from.origin.x, to: to.origin.x, progress: progress) 21 | let y = lerp(from: from.origin.y, to: to.origin.y, progress: progress) 22 | let w = lerp(from: from.size.width, to: to.size.width, progress: progress) 23 | let h = lerp(from: from.size.height, to: to.size.height, progress: progress) 24 | return CGRect(x: x, y: y, width: w, height: h) 25 | } 26 | } 27 | 28 | public enum Progress { 29 | 30 | public static func nomalizeProgress(min: T, max: T, progress: T) -> T { 31 | let p = progress.limited(min, max) 32 | let current = (p - min) 33 | return current / (max - min) 34 | } 35 | 36 | public static func centeredProgress(progress: T, centerRange: T) -> T { 37 | 38 | let normalProgress = progress.limited(0, 1) 39 | 40 | let p1 = (1 - centerRange)/2 41 | let p2 = (1 + centerRange)/2 42 | let centeredProgress: T 43 | switch progress { 44 | case let x where x < p1: 45 | centeredProgress = normalProgress / p1 46 | case let x where x <= p2: 47 | centeredProgress = 1 48 | case let x where x > p2: 49 | centeredProgress = 1 - (normalProgress - p2) / p1 50 | default: 51 | centeredProgress = 0 52 | } 53 | return centeredProgress 54 | } 55 | } 56 | 57 | 58 | public enum Inertia { 59 | 60 | public static func applyResistance(for source: CGFloat, with scrollPosition: CGFloat, decelerationRate: UIScrollView.DecelerationRate = .fast, maximumScrollDistance: CGFloat = 120) -> CGFloat { 61 | let resistantDistance = (decelerationRate.rawValue * abs(scrollPosition) * maximumScrollDistance) / (maximumScrollDistance + decelerationRate.rawValue * abs(scrollPosition)) 62 | return source + (scrollPosition < 0 ? -resistantDistance : resistantDistance) 63 | } 64 | 65 | // Distance travelled after deceleration to zero velocity at a constant rate 66 | public static func project(initialVelocity: CGFloat, decelerationRate: UIScrollView.DecelerationRate = .fast) -> CGFloat { 67 | return (initialVelocity / 1000.0) * decelerationRate.rawValue / (1.0 - decelerationRate.rawValue) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /utopia/Source/Utilities/Then.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015 Suyeol Jeon (xoul.kr) 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | import Foundation 24 | import CoreGraphics 25 | #if os(iOS) || os(tvOS) 26 | import UIKit.UIGeometry 27 | #endif 28 | 29 | public protocol Then {} 30 | 31 | extension Then where Self: Any { 32 | 33 | /// Makes it available to set properties with closures just after initializing and copying the value types. 34 | /// 35 | /// let frame = CGRect().with { 36 | /// $0.origin.x = 10 37 | /// $0.size.width = 100 38 | /// } 39 | public func with(_ block: (inout Self) throws -> Void) rethrows -> Self { 40 | var copy = self 41 | try block(©) 42 | return copy 43 | } 44 | 45 | /// Makes it available to execute something with closures. 46 | /// 47 | /// UserDefaults.standard.do { 48 | /// $0.set("devxoul", forKey: "username") 49 | /// $0.set("devxoul@gmail.com", forKey: "email") 50 | /// $0.synchronize() 51 | /// } 52 | public func `do`(_ block: (Self) throws -> Void) rethrows { 53 | try block(self) 54 | } 55 | 56 | } 57 | 58 | extension Then where Self: AnyObject { 59 | 60 | /// Makes it available to set properties with closures just after initializing. 61 | /// 62 | /// let label = UILabel().then { 63 | /// $0.textAlignment = .Center 64 | /// $0.textColor = UIColor.blackColor() 65 | /// $0.text = "Hello, World!" 66 | /// } 67 | public func then(_ block: (Self) throws -> Void) rethrows -> Self { 68 | try block(self) 69 | return self 70 | } 71 | 72 | } 73 | 74 | extension NSObject: Then {} 75 | 76 | extension CGPoint: Then {} 77 | extension CGRect: Then {} 78 | extension CGSize: Then {} 79 | extension CGVector: Then {} 80 | 81 | #if os(iOS) || os(tvOS) 82 | extension UIEdgeInsets: Then {} 83 | extension UIOffset: Then {} 84 | extension UIRectEdge: Then {} 85 | #endif 86 | -------------------------------------------------------------------------------- /utopia/Source/CodingDeconding/CodableUtilities.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct Safe: Decodable { 4 | public let value: Base? 5 | 6 | public init(from decoder: Decoder) throws { 7 | do { 8 | let container = try decoder.singleValueContainer() 9 | self.value = try container.decode(Base.self) 10 | } catch { 11 | assertionFailure("ERROR: \(error)") 12 | // TODO: automatically send a report about a corrupted data 13 | self.value = nil 14 | } 15 | } 16 | } 17 | 18 | 19 | extension KeyedDecodingContainer { 20 | public func decode(_ key: Key, as type: T.Type = T.self) throws -> T { 21 | return try self.decode(T.self, forKey: key) 22 | } 23 | 24 | public func decodeIfPresent(_ key: KeyedDecodingContainer.Key) throws -> T? { 25 | return try decodeIfPresent(T.self, forKey: key) 26 | } 27 | } 28 | 29 | extension KeyedDecodingContainer { 30 | public func decodeSafely(_ key: KeyedDecodingContainer.Key) -> T? { 31 | return self.decodeSafely(T.self, forKey: key) 32 | } 33 | 34 | public func decodeSafely(_ type: T.Type, forKey key: KeyedDecodingContainer.Key) -> T? { 35 | let decoded = try? decode(Safe.self, forKey: key) 36 | return decoded?.value 37 | } 38 | 39 | public func decodeSafelyIfPresent(_ key: KeyedDecodingContainer.Key) -> T? { 40 | return self.decodeSafelyIfPresent(T.self, forKey: key) 41 | } 42 | 43 | public func decodeSafelyIfPresent(_ type: T.Type, forKey key: KeyedDecodingContainer.Key) -> T? { 44 | let decoded = ((try? decodeIfPresent(Safe.self, forKey: key)) as Safe??) 45 | return decoded??.value 46 | } 47 | } 48 | 49 | extension KeyedDecodingContainer { 50 | public func decodeSafelyArray(of type: T.Type, forKey key: KeyedDecodingContainer.Key) -> [T] { 51 | let array = decodeSafely([Safe].self, forKey: key) 52 | return array?.compactMap { $0.value } ?? [] 53 | } 54 | } 55 | 56 | extension JSONDecoder { 57 | public func decodeSafelyArray(of type: T.Type, from data: Data) -> [T] { 58 | guard let array = try? decode([Safe].self, from: data) else { return [] } 59 | return array.compactMap { $0.value } 60 | } 61 | } 62 | 63 | 64 | public struct Id: Hashable { 65 | public let raw: String 66 | public init(_ raw: String) { 67 | self.raw = raw 68 | } 69 | 70 | public func hash(into hasher: inout Hasher) { 71 | hasher.combine(raw.hashValue) 72 | } 73 | 74 | public static func ==(lhs: Id, rhs: Id) -> Bool { 75 | return lhs.raw == rhs.raw 76 | } 77 | } 78 | 79 | extension Id: Codable { 80 | public init(from decoder: Decoder) throws { 81 | let container = try decoder.singleValueContainer() 82 | let raw = try container.decode(String.self) 83 | if raw.isEmpty { 84 | throw DecodingError.dataCorruptedError( 85 | in: container, 86 | debugDescription: "Cannot initialize Id from an empty string" 87 | ) 88 | } 89 | self.init(raw) 90 | } 91 | 92 | public func encode(to encoder: Encoder) throws { 93 | var container = encoder.singleValueContainer() 94 | try container.encode(raw) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /utopia/Source/UIKit/Extensions/CoreGraphics+ext.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | public extension CGSize { 4 | 5 | init(_ value: CGFloat) { 6 | self.init(width: value, height: value) 7 | } 8 | 9 | var asRect: CGRect { 10 | return CGRect(size: self) 11 | } 12 | 13 | init(_ width: CGFloat, _ height: CGFloat) { 14 | self.init(width: width, height: height) 15 | } 16 | 17 | func centered(in rect: CGRect) -> CGRect { 18 | var result = self.asRect 19 | result.origin.x = (rect.width - width) / 2 20 | result.origin.y = (rect.height - height) / 2 21 | return result 22 | } 23 | 24 | func centered(in size: CGSize) -> CGRect { 25 | return centered(in: CGRect(origin: .zero, size: size)) 26 | } 27 | 28 | var asPixelsForMainScreen: CGSize { 29 | return self * UIScreen.main.scale 30 | } 31 | 32 | var center: CGPoint { 33 | return CGPoint(x: width / 2, y: height / 2) 34 | } 35 | 36 | static var greatestFiniteMagnitude: CGSize { 37 | return CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude) 38 | } 39 | 40 | var area: CGFloat { 41 | return width * height 42 | } 43 | } 44 | 45 | public extension UIEdgeInsets { 46 | init(value: CGFloat) { 47 | self.init(top: value, left: value, bottom: value, right: value) 48 | } 49 | } 50 | 51 | public extension CGPoint { 52 | 53 | init(_ x: CGFloat, _ y: CGFloat) { 54 | self.init(x: x, y: y) 55 | } 56 | 57 | } 58 | 59 | public extension CGRect { 60 | 61 | init(size: CGSize) { 62 | self.init(origin: .zero, size: size) 63 | } 64 | 65 | var center: CGPoint { 66 | return CGPoint(x: midX, y: midY) 67 | } 68 | 69 | func insetBy(insets: UIEdgeInsets) -> CGRect { 70 | let x = origin.x + insets.left 71 | let y = origin.y + insets.top 72 | let w = size.width - insets.left - insets.right 73 | let h = size.height - insets.top - insets.bottom 74 | return CGRect(x: x, y: y, width: w, height: h) 75 | } 76 | } 77 | 78 | public extension CGAffineTransform { 79 | 80 | init(scale: CGFloat) { 81 | self.init(scaleX: scale, y: scale) 82 | } 83 | } 84 | 85 | public func + (lhs: CGPoint, rhs: CGPoint) -> CGPoint { 86 | return CGPoint(x: lhs.x + rhs.x, y: lhs.y + rhs.y) 87 | } 88 | 89 | public func += (lhs: inout CGPoint, rhs: CGPoint) { 90 | lhs = lhs + rhs 91 | } 92 | 93 | public func - (lhs: CGPoint, rhs: CGPoint) -> CGPoint { 94 | return CGPoint(x: lhs.x - rhs.x, y: lhs.y - rhs.y) 95 | } 96 | 97 | public func -= (lhs: inout CGPoint, rhs: CGPoint) { 98 | lhs = lhs - rhs 99 | } 100 | 101 | public func * (lhs: CGPoint, rhs: CGPoint) -> CGPoint { 102 | return CGPoint(x: lhs.x * rhs.x, y: lhs.y * rhs.y) 103 | } 104 | 105 | public func *= (lhs: inout CGPoint, rhs: CGPoint) { 106 | lhs = lhs * rhs 107 | } 108 | 109 | public func / (lhs: CGPoint, rhs: CGPoint) -> CGPoint { 110 | return CGPoint(x: lhs.x / rhs.x, y: lhs.y / rhs.y) 111 | } 112 | 113 | public func /= (lhs: inout CGPoint, rhs: CGPoint) { 114 | lhs = lhs / rhs 115 | } 116 | 117 | public func * (lhs: CGSize, rhs: CGFloat) -> CGSize { 118 | return CGSize(lhs.width * rhs, lhs.height * rhs) 119 | } 120 | -------------------------------------------------------------------------------- /utopia/Source/UIKit/Extensions/UIImage+Gradient.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import CoreImage 3 | 4 | extension UIImage { 5 | 6 | public enum Direction { 7 | case right 8 | case left 9 | case up 10 | case down 11 | } 12 | 13 | public static func gradientImage(startColor: UIColor, 14 | endColor: UIColor, 15 | size: CGSize, 16 | direction: Direction) -> UIImage { 17 | 18 | return UIImage.gradientImage(colors: [startColor, endColor], 19 | locations: [0,1], 20 | size: size, 21 | direction: direction) 22 | } 23 | 24 | 25 | public static func gradientImage(colors: [UIColor], 26 | locations: [CGFloat], 27 | size: CGSize, 28 | direction: Direction) -> UIImage { 29 | 30 | var startPoint = CGPoint() 31 | var endPoint = CGPoint() 32 | switch direction { 33 | case .left: 34 | startPoint = CGPoint() 35 | endPoint = CGPoint(x: 1.0, y: 0.0) 36 | case .right: 37 | startPoint = CGPoint(x: 1.0, y: 0.0) 38 | endPoint = CGPoint() 39 | case .up: 40 | startPoint = CGPoint(x: 0.0, y: 1.0) 41 | endPoint = CGPoint() 42 | case .down: 43 | startPoint = CGPoint() 44 | endPoint = CGPoint(x: 0.0, y: 1.0) 45 | } 46 | 47 | return UIImage.gradientImage(colors: colors, 48 | locations: locations, 49 | startPoint: startPoint, 50 | endPoint: endPoint, size: size) 51 | } 52 | 53 | 54 | public static func gradientImage(colors: [UIColor], locations: [CGFloat], startPoint: CGPoint, endPoint: CGPoint, size: CGSize) -> UIImage { 55 | UIGraphicsBeginImageContext(size) 56 | defer { UIGraphicsEndImageContext() } 57 | 58 | guard let context = UIGraphicsGetCurrentContext() else { return UIImage() } 59 | UIGraphicsPushContext(context) 60 | 61 | let components = colors.reduce([]) { (currentResult: [CGFloat], currentColor: UIColor) -> [CGFloat] in 62 | var result = currentResult 63 | 64 | guard let components = currentColor.cgColor.components else { return result } 65 | if currentColor.cgColor.numberOfComponents == 2 { 66 | result.append(contentsOf: [components[0], components[0], components[0], components[1]]) 67 | } else { 68 | result.append(contentsOf:[components[0], components[1], components[2], components[3]]) 69 | } 70 | 71 | return result 72 | } 73 | 74 | guard let gradient = CGGradient(colorSpace: CGColorSpaceCreateDeviceRGB(), colorComponents: components, locations: locations, count: colors.count) else { return UIImage() } 75 | 76 | let transformedStartPoint = CGPoint(x: startPoint.x * size.width, y: startPoint.y * size.height) 77 | let transformedEndPoint = CGPoint(x: endPoint.x * size.width, y: endPoint.y * size.height) 78 | context.drawLinearGradient(gradient, start: transformedStartPoint, end: transformedEndPoint, options: []) 79 | UIGraphicsPopContext() 80 | let gradientImage = UIGraphicsGetImageFromCurrentImageContext() 81 | 82 | return gradientImage ?? UIImage() 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /utopia/Source/Utilities/TimingFunction.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | public enum TimingFunction { 5 | case linear, easeIn, easeOut, easeInOut, spring, 6 | easeInSine, easeOutSine, easeInOutSine, 7 | easeInQuad, easeOutQuad, easeInOutQuad, 8 | easeInCubic, easeOutCubic, easeInOutCubic, 9 | easeInQuart, easeOutQuart, easeInOutQuart, 10 | easeInQuint, easeOutQuint, easeInOutQuint, 11 | easeInExpo, easeOutExpo, easeInOutExpo, 12 | easeInCirc, easeOutCirc, easeInOutCirc, 13 | easeInBack, easeOutBack, easeInOutBack 14 | } 15 | 16 | extension CAMediaTimingFunction { 17 | public static func timingFunction(withCurve curve: TimingFunction) -> CAMediaTimingFunction { 18 | switch curve { 19 | case .linear: 20 | return CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear) 21 | case .easeIn: 22 | return CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeIn) 23 | case .easeOut: 24 | return CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut) 25 | case .easeInOut: 26 | return CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut) 27 | case .spring: 28 | return CAMediaTimingFunction(controlPoints: 0.5, 1.1 + Float(1/3), 1, 1) 29 | case .easeInSine: 30 | return CAMediaTimingFunction(controlPoints: 0.47, 0, 0.745, 0.715) 31 | case .easeOutSine: 32 | return CAMediaTimingFunction(controlPoints: 0.39, 0.575, 0.565, 1) 33 | case .easeInOutSine: 34 | return CAMediaTimingFunction(controlPoints: 0.445, 0.05, 0.55, 0.95) 35 | case .easeInQuad: 36 | return CAMediaTimingFunction(controlPoints: 0.55, 0.085, 0.68, 0.53) 37 | case .easeOutQuad: 38 | return CAMediaTimingFunction(controlPoints: 0.25, 0.46, 0.45, 0.94) 39 | case .easeInOutQuad: 40 | return CAMediaTimingFunction(controlPoints: 0.455, 0.03, 0.515, 0.955) 41 | case .easeInCubic: 42 | return CAMediaTimingFunction(controlPoints: 0.55, 0.055, 0.675, 0.19) 43 | case .easeOutCubic: 44 | return CAMediaTimingFunction(controlPoints: 0.215, 0.61, 0.355, 1) 45 | case .easeInOutCubic: 46 | return CAMediaTimingFunction(controlPoints: 0.645, 0.045, 0.355, 1) 47 | case .easeInQuart: 48 | return CAMediaTimingFunction(controlPoints: 0.895, 0.03, 0.685, 0.22) 49 | case .easeOutQuart: 50 | return CAMediaTimingFunction(controlPoints: 0.165, 0.84, 0.44, 1) 51 | case .easeInOutQuart: 52 | return CAMediaTimingFunction(controlPoints: 0.77, 0, 0.175, 1) 53 | case .easeInQuint: 54 | return CAMediaTimingFunction(controlPoints: 0.755, 0.05, 0.855, 0.06) 55 | case .easeOutQuint: 56 | return CAMediaTimingFunction(controlPoints: 0.23, 1, 0.32, 1) 57 | case .easeInOutQuint: 58 | return CAMediaTimingFunction(controlPoints: 0.86, 0, 0.07, 1) 59 | case .easeInExpo: 60 | return CAMediaTimingFunction(controlPoints: 0.95, 0.05, 0.795, 0.035) 61 | case .easeOutExpo: 62 | return CAMediaTimingFunction(controlPoints: 0.19, 1, 0.22, 1) 63 | case .easeInOutExpo: 64 | return CAMediaTimingFunction(controlPoints: 1, 0, 0, 1) 65 | case .easeInCirc: 66 | return CAMediaTimingFunction(controlPoints: 0.6, 0.04, 0.98, 0.335) 67 | case .easeOutCirc: 68 | return CAMediaTimingFunction(controlPoints: 0.075, 0.82, 0.165, 1) 69 | case .easeInOutCirc: 70 | return CAMediaTimingFunction(controlPoints: 0.785, 0.135, 0.15, 0.86) 71 | case .easeInBack: 72 | return CAMediaTimingFunction(controlPoints: 0.6, -0.28, 0.735, 0.045) 73 | case .easeOutBack: 74 | return CAMediaTimingFunction(controlPoints: 0.175, 0.885, 0.32, 1.275) 75 | case .easeInOutBack: 76 | return CAMediaTimingFunction(controlPoints: 0.68, -0.55, 0.265, 1.55) 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /utopia/Source/OptimizationTricks/SwiftyImageView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | public class SwiftyImageView: UIView { 4 | 5 | public var image: UIImage? { 6 | get { return backgroundImage } 7 | set { 8 | backgroundImage = newValue 9 | } 10 | } 11 | 12 | private init(_ image: UIImage? = nil) { 13 | super.init(frame: .zero) 14 | self.image = image 15 | isUserInteractionEnabled = false 16 | } 17 | 18 | public required init?(coder aDecoder: NSCoder) { 19 | fatalError("init(coder:) has not been implemented") 20 | } 21 | 22 | public final class func load(_ image: UIImage? = nil) -> SwiftyImageView { 23 | return SwiftyImageView(image) 24 | } 25 | } 26 | 27 | public extension SwiftyImageView { 28 | 29 | enum ImageTransition { 30 | case noTransition 31 | case crossDissolve(TimeInterval) 32 | case curlDown(TimeInterval) 33 | case curlUp(TimeInterval) 34 | case flipFromBottom(TimeInterval) 35 | case flipFromLeft(TimeInterval) 36 | case flipFromRight(TimeInterval) 37 | case flipFromTop(TimeInterval) 38 | case custom( 39 | duration: TimeInterval, 40 | animationOptions: UIView.AnimationOptions, 41 | animations: (SwiftyImageView, UIImage) -> Void, 42 | completion: ((Bool) -> Void)? 43 | ) 44 | 45 | /// The duration of the image transition in seconds. 46 | public var duration: TimeInterval { 47 | switch self { 48 | case .noTransition: 49 | return 0.0 50 | case .crossDissolve(let duration): 51 | return duration 52 | case .curlDown(let duration): 53 | return duration 54 | case .curlUp(let duration): 55 | return duration 56 | case .flipFromBottom(let duration): 57 | return duration 58 | case .flipFromLeft(let duration): 59 | return duration 60 | case .flipFromRight(let duration): 61 | return duration 62 | case .flipFromTop(let duration): 63 | return duration 64 | case .custom(let duration, _, _, _): 65 | return duration 66 | } 67 | } 68 | 69 | /// The animation options of the image transition. 70 | public var animationOptions: UIView.AnimationOptions { 71 | switch self { 72 | case .noTransition: 73 | return UIView.AnimationOptions() 74 | case .crossDissolve: 75 | return .transitionCrossDissolve 76 | case .curlDown: 77 | return .transitionCurlDown 78 | case .curlUp: 79 | return .transitionCurlUp 80 | case .flipFromBottom: 81 | return .transitionFlipFromBottom 82 | case .flipFromLeft: 83 | return .transitionFlipFromLeft 84 | case .flipFromRight: 85 | return .transitionFlipFromRight 86 | case .flipFromTop: 87 | return .transitionFlipFromTop 88 | case .custom(_, let animationOptions, _, _): 89 | return animationOptions 90 | } 91 | } 92 | 93 | public var animations: ((SwiftyImageView, UIImage) -> Void) { 94 | switch self { 95 | case .custom(_, _, let animations, _): 96 | return animations 97 | default: 98 | return { $0.image = $1 } 99 | } 100 | } 101 | 102 | public var completion: ((Bool) -> Void)? { 103 | switch self { 104 | case .custom(_, _, _, let completion): 105 | return completion 106 | default: 107 | return nil 108 | } 109 | } 110 | } 111 | 112 | final func transition(_ imageTransition: ImageTransition, with image: UIImage) { 113 | 114 | UIView.transition(with: self, duration: imageTransition.duration, options: imageTransition.animationOptions, animations: { imageTransition.animations(self, image) }, completion: imageTransition.completion) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /utopia/Source/Validation/Validation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Validation.swift 3 | // Vendefy 4 | // 5 | // Created by Dmitriy Kalachev on 4/17/18. 6 | // Copyright © 2018 Ramotion. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public protocol ValidatorProtocol { 12 | associatedtype Value 13 | func validate(_ value: Value) -> ValidationResult 14 | } 15 | 16 | public extension ValidatorProtocol { 17 | func asValidator() -> Validator { 18 | return Validator(self) 19 | } 20 | } 21 | 22 | public struct Validator: ValidatorProtocol { 23 | private let baseValidate: (Value) -> ValidationResult 24 | 25 | public init(_ base: V) where V.Value == Value { 26 | baseValidate = base.validate 27 | } 28 | 29 | public static func block(validator: @escaping (T) -> ValidationResult) -> Validator { 30 | return Validator(block: validator) 31 | } 32 | 33 | public static func block(message: String, block: @escaping (T) -> Bool) -> Validator { 34 | return Validator(block: { 35 | if block($0) { return .valid } 36 | return .invalid(message) 37 | }) 38 | } 39 | 40 | public static func block(bool: @escaping (T) -> Bool) -> Validator { 41 | return Validator(block: { 42 | if bool($0) { return .valid } 43 | return .invalid("invalid") 44 | }) 45 | } 46 | 47 | init(block: @escaping (Value) -> ValidationResult) { 48 | baseValidate = block 49 | } 50 | 51 | public func validate(_ value: Value) -> ValidationResult { 52 | return baseValidate(value) 53 | } 54 | } 55 | 56 | public enum ValidationResult { 57 | 58 | case valid 59 | case invalid(String) 60 | 61 | public var isValid: Bool { 62 | switch self { 63 | case .valid: return true 64 | case .invalid: return false 65 | } 66 | } 67 | 68 | public var error: String? { 69 | switch self { 70 | case .valid: return nil 71 | case .invalid(let error): return error 72 | } 73 | } 74 | 75 | public static func && (left: ValidationResult, right: ValidationResult) -> ValidationResult { 76 | if left.isValid && right.isValid { 77 | return .valid 78 | } 79 | 80 | return .invalid(left.error ?? right.error!) 81 | } 82 | 83 | public static func || (left: ValidationResult, right: ValidationResult) -> ValidationResult { 84 | if left.isValid || right.isValid { 85 | return .valid 86 | } 87 | 88 | return .invalid(left.error ?? right.error!) 89 | } 90 | 91 | } 92 | 93 | 94 | // MARK: - Validators Composition 95 | 96 | public enum CompoundValidator: ValidatorProtocol { 97 | case and(Validator, Validator) 98 | case or(Validator, Validator) 99 | 100 | public func validate(_ value: Value) -> ValidationResult { 101 | switch self { 102 | case let .and(left, right): 103 | return left.validate(value) && right.validate(value) 104 | 105 | case let .or(left, right): 106 | return left.validate(value) || right.validate(value) 107 | } 108 | } 109 | 110 | } 111 | 112 | public func && (left: V1, right: V2) -> CompoundValidator 113 | where V1.Value == V2.Value 114 | { 115 | return .and(Validator(left), Validator(right)) 116 | } 117 | 118 | public func || (left: V1, right: V2) -> CompoundValidator 119 | where V1.Value == V2.Value 120 | { 121 | return .or(Validator(left), Validator(right)) 122 | } 123 | 124 | public func && (left: Validator, right: Validator) -> Validator 125 | { 126 | return CompoundValidator.and(left, right).asValidator() 127 | } 128 | 129 | public func || (left: Validator, right: Validator) -> Validator 130 | { 131 | return CompoundValidator.or(left, right).asValidator() 132 | } 133 | -------------------------------------------------------------------------------- /Example/utopia/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 25 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /utopia/Source/Transitions/ScaleModalTransitioning.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | public final class ScaleModalPresentAC: NSObject, UIViewControllerAnimatedTransitioning { 5 | public func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { 6 | return TimeInterval.halfSecond 7 | } 8 | 9 | public func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { 10 | 11 | let duration = transitionDuration(using: transitionContext) 12 | 13 | //check custom ainmation conditions 14 | guard let toVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to), 15 | let fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from), 16 | let fromSnapshot = fromVC.view.snapshotView() else { 17 | 18 | CustomTransitionAnimation.alphaPresent(using: transitionContext, duration: duration) 19 | return 20 | } 21 | 22 | let containerView = transitionContext.containerView 23 | containerView.addSubview(fromSnapshot) 24 | fromSnapshot.frame = fromVC.view.frame 25 | fromVC.view.isHidden = true 26 | 27 | 28 | toVC.view.frame = transitionContext.finalFrame(for: toVC) 29 | containerView.addSubview(toVC.view) 30 | toVC.view.layoutIfNeeded() 31 | toVC.view.transform = CGAffineTransform(translationX: 0, y: containerView.bounds.size.height) 32 | 33 | 34 | UIView.animateKeyframes(withDuration: duration, delay: 0, options: [], animations: { 35 | 36 | UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.8, animations: { 37 | fromSnapshot.transform = CGAffineTransform(scale: 0.7) 38 | fromSnapshot.alpha = 0 39 | }) 40 | 41 | UIView.addKeyframe(withRelativeStartTime: 0.3, relativeDuration: 0.7, animations: { 42 | toVC.view.transform = CGAffineTransform.identity 43 | }) 44 | 45 | }, completion: { _ in 46 | fromSnapshot.removeFromSuperview() 47 | fromVC.view.isHidden = false 48 | transitionContext.completeTransition(!transitionContext.transitionWasCancelled) 49 | }) 50 | } 51 | } 52 | 53 | 54 | public final class ScaleModalDismissAC: NSObject, UIViewControllerAnimatedTransitioning { 55 | 56 | public func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { 57 | return TimeInterval.halfSecond 58 | } 59 | 60 | public func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { 61 | 62 | let duration = transitionDuration(using: transitionContext) 63 | 64 | //check custom ainmation conditions 65 | guard let toVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to), 66 | let fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from) else { 67 | 68 | CustomTransitionAnimation.alphaDismiss(using: transitionContext, duration: duration) 69 | return 70 | } 71 | 72 | let containerView = transitionContext.containerView 73 | toVC.view.frame = transitionContext.finalFrame(for: toVC) 74 | toVC.view.layoutIfNeeded() 75 | toVC.view.transform = CGAffineTransform(scaleX: 0.7, y: 0.7) 76 | toVC.view.alpha = 0 77 | 78 | UIView.animateKeyframes(withDuration: duration, delay: 0, options: [], animations: { 79 | 80 | UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 0.6, animations: { 81 | fromVC.view.transform = CGAffineTransform(translationX: 0, y: containerView.bounds.size.height) 82 | }) 83 | 84 | UIView.addKeyframe(withRelativeStartTime: 0.2, relativeDuration: 0.6, animations: { 85 | toVC.view.transform = CGAffineTransform.identity 86 | toVC.view.alpha = 1 87 | }) 88 | 89 | }, completion: { _ in 90 | fromVC.view.removeFromSuperview() 91 | fromVC.removeFromParent() 92 | transitionContext.completeTransition(!transitionContext.transitionWasCancelled) 93 | }) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /utopia/Source/UIKit/Toggler.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | public protocol Togglable: class { 5 | var isOn: Bool { get } 6 | func selectedToggle(select: Bool) 7 | } 8 | 9 | extension UIControl: Togglable { 10 | 11 | public var isOn: Bool { return isSelected } 12 | 13 | @objc public func selectedToggle(select: Bool) { 14 | isSelected = select 15 | } 16 | } 17 | 18 | #if !os(tvOS) 19 | extension UISwitch { 20 | public override func selectedToggle(select: Bool) { 21 | setOn(select, animated: true) 22 | } 23 | } 24 | #endif 25 | 26 | public struct Toggler { 27 | var togglers = [Togglable]() 28 | 29 | public var index: Int { 30 | return togglers.firstIndex(where: { $0.isOn }) ?? 0 31 | } 32 | 33 | public init(default index: Int = 0, togglers: [Togglable]) { 34 | self.togglers = togglers 35 | onAt(index: index) 36 | } 37 | 38 | public func on(toggle: Togglable) { 39 | toggleControl(toggle: toggle, togglers: togglers) 40 | } 41 | 42 | public func onAt(index: Int) { 43 | if let toggler = togglers.at(index) { 44 | toggleControl(toggle: toggler, togglers: togglers) 45 | } 46 | } 47 | 48 | public mutating func add(toggle: Togglable) { 49 | togglers.append(toggle) 50 | } 51 | 52 | public mutating func remove(at index: Int) { 53 | guard index < togglers.count else { 54 | fatalError("Index is out of array") 55 | } 56 | togglers.remove(at: index) 57 | } 58 | 59 | private func toggleControl(toggle: Togglable, togglers: [Togglable]) { 60 | togglers.enumerated().forEach { 61 | toggleStatus(toggle: $0.element, on: $0.element === toggle) 62 | } 63 | } 64 | 65 | private func toggleStatus(toggle: Togglable, on: Bool) { 66 | toggle.selectedToggle(select: on) 67 | } 68 | } 69 | 70 | public class ButtonsTogglerView: UIStackView { 71 | public var onToggle: ((Int) -> Void)? = nil 72 | private let toggler: Toggler 73 | public private(set) var currentIndex: Int 74 | 75 | public required init(defaultIndex index: Int = 0, 76 | buttons: [UIButton], 77 | axis: NSLayoutConstraint.Axis = .horizontal, 78 | spacing: CGFloat = 30, 79 | distribution: UIStackView.Distribution = .equalSpacing, 80 | alignment: UIStackView.Alignment = .fill) { 81 | 82 | currentIndex = index 83 | toggler = Toggler(default: index, togglers: buttons) 84 | super.init(frame: CGRect.zero) 85 | addArrangedSubviews(buttons) 86 | 87 | self.axis = axis 88 | self.alignment = alignment 89 | self.distribution = distribution 90 | self.spacing = spacing 91 | 92 | for (index, button) in buttons.enumerated() { 93 | button.onTouchUpInside.subscribe(with: self, callback: {[unowned self] _ in 94 | guard index != self.currentIndex else { return } 95 | 96 | self.currentIndex = index 97 | self.toggler.onAt(index: index) 98 | self.onToggle?(index) 99 | }) 100 | } 101 | } 102 | 103 | public required init(coder: NSCoder) { 104 | fatalError("init(coder:) has not been implemented") 105 | } 106 | } 107 | 108 | public class ButtonsToggler { 109 | public var onToggle: ((Int) -> Void)? = nil 110 | private let toggler: Toggler 111 | public private(set) var currentIndex: Int 112 | 113 | public func selectIndex(at index: Int) { 114 | guard index >= 0 && index < toggler.togglers.count else { return } 115 | toggler.onAt(index: index) 116 | currentIndex = index 117 | } 118 | 119 | public init(defaultIndex index: Int = 0, buttons: [UIButton]) { 120 | currentIndex = index 121 | toggler = Toggler(default: index, togglers: buttons) 122 | 123 | for (index, button) in buttons.enumerated() { 124 | button.onTouchUpInside.subscribe(with: self, callback: {[unowned self] _ in 125 | guard index != self.currentIndex else { return } 126 | 127 | self.currentIndex = index 128 | self.toggler.onAt(index: index) 129 | self.onToggle?(index) 130 | }) 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /utopia/Source/Signals/SignalSubscription.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SignalSubscription.swift 3 | // utopia 4 | // 5 | // Created by Dmitriy Kalachev on 4/18/18. 6 | // Copyright © 2018 Ramotion. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// A SignalLister represenents an instance and its association with a `Signal`. 12 | final public class SignalSubscription { 13 | public typealias SignalCallback = (T) -> Void 14 | public typealias SignalFilter = (T) -> Bool 15 | 16 | // The observer. 17 | weak public var observer: AnyObject? 18 | 19 | /// Whether the observer should be removed once it observes the `Signal` firing once. Defaults to false. 20 | public var once = false 21 | 22 | fileprivate var queuedData: T? 23 | var filter: (SignalFilter)? 24 | fileprivate var callback: SignalCallback 25 | fileprivate var dispatchQueue: DispatchQueue? 26 | private var sampleInterval: TimeInterval? 27 | 28 | init(observer: AnyObject, callback: @escaping SignalCallback) { 29 | self.observer = observer 30 | self.callback = callback 31 | } 32 | 33 | /// Assigns a filter to the `SignalSubscription`. This lets you define conditions under which a observer should actually 34 | /// receive the firing of a `Singal`. The closure that is passed an argument can decide whether the firing of a 35 | /// `Signal` should actually be dispatched to its observer depending on the data fired. 36 | /// 37 | /// If the closeure returns true, the observer is informed of the fire. The default implementation always 38 | /// returns `true`. 39 | /// 40 | /// - parameter predicate: A closure that can decide whether the `Signal` fire should be dispatched to its observer. 41 | /// - returns: Returns self so you can chain calls. 42 | @discardableResult 43 | public func filter(_ predicate: @escaping SignalFilter) -> SignalSubscription { 44 | self.filter = predicate 45 | return self 46 | } 47 | 48 | 49 | /// Tells the observer to sample received `Signal` data and only dispatch the latest data once the time interval 50 | /// has elapsed. This is useful if the subscriber wants to throttle the amount of data it receives from the 51 | /// `Singla`. 52 | /// 53 | /// - parameter sampleInterval: The number of seconds to delay dispatch. 54 | /// - returns: Returns self so you can chain calls. 55 | @discardableResult 56 | public func sample(every sampleInterval: TimeInterval) -> SignalSubscription { 57 | self.sampleInterval = sampleInterval 58 | return self 59 | } 60 | 61 | /// Assigns a dispatch queue to the `SignalSubscription`. The queue is used for scheduling the observer calls. If not 62 | /// nil, the callback is fired asynchronously on the specified queue. Otherwise, the block is run synchronously 63 | /// on the posting thread, which is its default behaviour. 64 | /// 65 | /// - parameter queue: A queue for performing the observer's calls. 66 | /// - returns: Returns self so you can chain calls. 67 | @discardableResult 68 | public func onQueue(_ queue: DispatchQueue) -> SignalSubscription { 69 | self.dispatchQueue = queue 70 | return self 71 | } 72 | 73 | /// Cancels the observer. This will cancelSubscription the listening object from the `Signal`. 74 | public func cancel() { 75 | self.observer = nil 76 | } 77 | 78 | // MARK: - Internal Interface 79 | 80 | func dispatch(data: T) -> Bool { 81 | guard observer != nil else { 82 | return false 83 | } 84 | 85 | if once { 86 | observer = nil 87 | } 88 | 89 | if let sampleInterval = sampleInterval { 90 | if queuedData != nil { 91 | queuedData = data 92 | } else { 93 | queuedData = data 94 | let block = { [weak self] () -> Void in 95 | if let definiteSelf = self { 96 | let data = definiteSelf.queuedData! 97 | definiteSelf.queuedData = nil 98 | if definiteSelf.observer != nil { 99 | definiteSelf.callback(data) 100 | } 101 | } 102 | } 103 | let dispatchQueue = self.dispatchQueue ?? DispatchQueue.main 104 | let deadline = DispatchTime.now() + DispatchTimeInterval.milliseconds(Int(sampleInterval * 1000)) 105 | dispatchQueue.asyncAfter(deadline: deadline, execute: block) 106 | } 107 | } else { 108 | if let queue = self.dispatchQueue { 109 | queue.async { 110 | self.callback(data) 111 | } 112 | } else { 113 | callback(data) 114 | } 115 | } 116 | 117 | return observer != nil 118 | } 119 | } 120 | 121 | -------------------------------------------------------------------------------- /utopia/Source/UIKit/Extensions/UIButton+layout.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | public extension UIButton { 4 | /// Enum to determine the title position with respect to the button image 5 | /// 6 | /// - top: title above button image 7 | /// - bottom: title below button image 8 | /// - left: title to the left of button image 9 | /// - right: title to the right of button image 10 | enum Position: Int { 11 | case top, bottom, left, right 12 | } 13 | 14 | /// This method sets an image and title for a UIButton and 15 | /// repositions the titlePosition with respect to the button image. 16 | /// 17 | /// - Parameters: 18 | /// - image: Button image 19 | /// - title: Button title 20 | /// - titlePosition: UIViewContentModeTop, UIViewContentModeBottom, UIViewContentModeLeft or UIViewContentModeRight 21 | /// - additionalSpacing: Spacing between image and title 22 | /// - state: State to apply this behaviour 23 | func set(image: UIImage?, title: String, titlePosition: Position, spacing: CGFloat, state: UIControl.State){ 24 | imageView?.contentMode = .center 25 | setImage(image, for: state) 26 | setTitle(title, for: state) 27 | titleLabel?.contentMode = .center 28 | 29 | adjust(title: title as NSString, at: titlePosition, spacing: spacing) 30 | 31 | } 32 | 33 | /// This method sets an image and an attributed title for a UIButton and 34 | /// repositions the titlePosition with respect to the button image. 35 | /// 36 | /// - Parameters: 37 | /// - image: Button image 38 | /// - title: Button attributed title 39 | /// - titlePosition: UIViewContentModeTop, UIViewContentModeBottom, UIViewContentModeLeft or UIViewContentModeRight 40 | /// - additionalSpacing: Spacing between image and title 41 | /// - state: State to apply this behaviour 42 | func set(image: UIImage?, attributedTitle title: NSAttributedString, titlePosition: Position, spacing: CGFloat, state: UIControl.State){ 43 | imageView?.contentMode = .center 44 | setImage(image, for: state) 45 | 46 | adjust(attributedTitle: title, at: titlePosition, spacing: spacing) 47 | 48 | titleLabel?.contentMode = .center 49 | setAttributedTitle(title, for: state) 50 | } 51 | 52 | 53 | // MARK: Private Methods 54 | 55 | private func adjust(title: NSString, at position: Position, spacing: CGFloat) { 56 | let imageRect: CGRect = self.imageRect(forContentRect: frame) 57 | 58 | // Use predefined font, otherwise use the default 59 | let titleFont: UIFont = titleLabel?.font ?? UIFont() 60 | let titleSize: CGSize = title.size(withAttributes: [NSAttributedString.Key.font: titleFont]) 61 | 62 | arrange(titleSize: titleSize, imageRect: imageRect, atPosition: position, spacing: spacing) 63 | } 64 | 65 | private func adjust(attributedTitle: NSAttributedString, at position: Position, spacing: CGFloat) { 66 | let imageRect: CGRect = self.imageRect(forContentRect: frame) 67 | let titleSize = attributedTitle.size() 68 | 69 | arrange(titleSize: titleSize, imageRect: imageRect, atPosition: position, spacing: spacing) 70 | } 71 | 72 | private func arrange(titleSize: CGSize, imageRect:CGRect, atPosition position: Position, spacing: CGFloat) { 73 | switch (position) { 74 | case .top: 75 | titleEdgeInsets = UIEdgeInsets(top: -(imageRect.height + titleSize.height + spacing), left: -(imageRect.width), bottom: 0, right: 0) 76 | imageEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: -titleSize.width) 77 | contentEdgeInsets = UIEdgeInsets(top: spacing / 2 + titleSize.height, left: -imageRect.width/2, bottom: 0, right: -imageRect.width/2) 78 | case .bottom: 79 | titleEdgeInsets = UIEdgeInsets(top: (imageRect.height + titleSize.height + spacing), left: -(imageRect.width), bottom: 0, right: 0) 80 | imageEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: -titleSize.width) 81 | contentEdgeInsets = UIEdgeInsets(top: 0, left: -imageRect.width/2, bottom: spacing / 2 + titleSize.height, right: -imageRect.width/2) 82 | case .left: 83 | titleEdgeInsets = UIEdgeInsets(top: 0, left: -(imageRect.width * 2), bottom: 0, right: 0) 84 | imageEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: -(titleSize.width * 2 + spacing)) 85 | contentEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: spacing / 2) 86 | case .right: 87 | titleEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: -spacing) 88 | imageEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) 89 | contentEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: spacing / 2) 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /utopia/Source/OptimizationTricks/SwiftyImage.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | public extension UIImage 4 | { 5 | final var isOpaque: Bool { 6 | let alphaInfo = cgImage?.alphaInfo 7 | return !(alphaInfo == .first || alphaInfo == .last || alphaInfo == .premultipliedFirst || alphaInfo == .premultipliedLast) 8 | } 9 | 10 | final func reSize(to size: CGSize) -> UIImage { 11 | guard size.width > 0 && size.height > 0 else { return self } 12 | 13 | UIGraphicsBeginImageContextWithOptions(size, isOpaque, 0.0) 14 | draw(in: CGRect(origin: .zero, size: size)) 15 | let scaledImage = UIGraphicsGetImageFromCurrentImageContext() ?? self 16 | UIGraphicsEndImageContext() 17 | 18 | return scaledImage 19 | } 20 | 21 | final func reSize(toFit size: CGSize) -> UIImage { 22 | guard size.width > 0 && size.height > 0 else { return self } 23 | 24 | let imageAspectRatio = self.size.width / self.size.height 25 | let canvasAspectRatio = size.width / size.height 26 | var resizeFactor: CGFloat 27 | if imageAspectRatio > canvasAspectRatio { 28 | resizeFactor = size.width / self.size.width 29 | } else { 30 | resizeFactor = size.height / self.size.height 31 | } 32 | let scaledSize = CGSize(width: self.size.width * resizeFactor, height: self.size.height * resizeFactor) 33 | let origin = CGPoint(x: (size.width - scaledSize.width) / 2.0, y: (size.height - scaledSize.height) / 2.0) 34 | 35 | UIGraphicsBeginImageContextWithOptions(size, false, 0.0) 36 | draw(in: CGRect(origin: origin, size: scaledSize)) 37 | let scaledImage = UIGraphicsGetImageFromCurrentImageContext() ?? self 38 | UIGraphicsEndImageContext() 39 | 40 | return scaledImage 41 | } 42 | 43 | final func reSize(toFill size: CGSize) -> UIImage { 44 | guard size.width > 0 && size.height > 0 else { return self } 45 | 46 | let imageAspectRatio = self.size.width / self.size.height 47 | let canvasAspectRatio = size.width / size.height 48 | var resizeFactor: CGFloat 49 | if imageAspectRatio > canvasAspectRatio { 50 | resizeFactor = size.height / self.size.height 51 | } else { 52 | resizeFactor = size.width / self.size.width 53 | } 54 | let scaledSize = CGSize(width: self.size.width * resizeFactor, height: self.size.height * resizeFactor) 55 | let origin = CGPoint(x: (size.width - scaledSize.width) / 2.0, y: (size.height - scaledSize.height) / 2.0) 56 | 57 | UIGraphicsBeginImageContextWithOptions(size, isOpaque, 0.0) 58 | draw(in: CGRect(origin: origin, size: scaledSize)) 59 | let scaledImage = UIGraphicsGetImageFromCurrentImageContext() ?? self 60 | UIGraphicsEndImageContext() 61 | 62 | return scaledImage 63 | } 64 | 65 | final func rounded(withCornerRadius radius: CGFloat, divideRadiusByImageScale: Bool = false) -> UIImage { 66 | UIGraphicsBeginImageContextWithOptions(size, false, 0.0) 67 | 68 | let scaledRadius = divideRadiusByImageScale ? radius / scale : radius 69 | 70 | let clippingPath = UIBezierPath(roundedRect: CGRect(origin: CGPoint.zero, size: size), cornerRadius: scaledRadius) 71 | clippingPath.addClip() 72 | 73 | draw(in: CGRect(origin: CGPoint.zero, size: size)) 74 | 75 | let roundedImage = UIGraphicsGetImageFromCurrentImageContext()! 76 | UIGraphicsEndImageContext() 77 | 78 | return roundedImage 79 | } 80 | 81 | final func roundedIntoCircle() -> UIImage { 82 | let radius = min(size.width, size.height) / 2.0 83 | var squareImage = self 84 | if size.width != size.height { 85 | let squareDimension = min(size.width, size.height) 86 | let squareSize = CGSize(width: squareDimension, height: squareDimension) 87 | squareImage = reSize(toFill: squareSize) 88 | } 89 | 90 | UIGraphicsBeginImageContextWithOptions(squareImage.size, false, 0.0) 91 | let clippingPath = UIBezierPath( roundedRect: CGRect(origin: CGPoint.zero, size: squareImage.size), cornerRadius: radius) 92 | clippingPath.addClip() 93 | squareImage.draw(in: CGRect(origin: CGPoint.zero, size: squareImage.size)) 94 | let roundedImage = UIGraphicsGetImageFromCurrentImageContext()! 95 | UIGraphicsEndImageContext() 96 | 97 | return roundedImage 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Example/utopia.xcodeproj/xcshareddata/xcschemes/utopia-Example.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 51 | 52 | 53 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 76 | 78 | 84 | 85 | 86 | 87 | 93 | 95 | 101 | 102 | 103 | 104 | 106 | 107 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /utopia/Source/UIKit/UISearchUtilities.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UIViewController { 4 | 5 | public func findUp() -> T? where T: UIViewController { 6 | return UISearchUtilities.findUpVC(self) 7 | } 8 | 9 | public func findDown() -> T? where T: UIViewController { 10 | return UISearchUtilities.findDownVC(self) 11 | } 12 | } 13 | 14 | 15 | struct UISearchUtilities { 16 | 17 | static func findUpVC(_ base: UIViewController) -> T? where T: UIViewController { 18 | 19 | var vc: UIViewController? = base 20 | 21 | while vc != nil { 22 | if let vc = vc?.parent as? T { 23 | return vc 24 | } else { 25 | vc = vc?.parent 26 | } 27 | } 28 | return nil 29 | } 30 | 31 | static func findDownVC(_ base: UIViewController) -> T? where T: UIViewController { 32 | var result: T? 33 | for c in base.children { 34 | if let r = c as? T { 35 | result = r 36 | } else { 37 | result = findDownVC(c) 38 | } 39 | if result != nil { 40 | break 41 | } 42 | } 43 | return result 44 | } 45 | 46 | static func findDownVC() -> T? where T: UIViewController { 47 | guard let base = UIApplication.shared.keyWindow?.rootViewController else { return nil } 48 | if let result = base as? T { 49 | return result 50 | } 51 | return findDownVC(base) 52 | } 53 | 54 | static func first(inView view: UIView, where condition: @escaping (UIView) -> Bool) -> UIView? { 55 | guard !condition(view) else { return view } 56 | 57 | var result: UIView? 58 | for v in view.subviews { 59 | if condition(v) { 60 | result = v 61 | } else { 62 | result = first(inView: v, where: condition) 63 | } 64 | if result != nil { 65 | break 66 | } 67 | } 68 | return result 69 | } 70 | 71 | static func findView(_ root: UIView) -> T? where T: UIView { 72 | var result: T? 73 | for v in root.subviews { 74 | if let r = v as? T { 75 | result = r 76 | } else { 77 | result = findView(v) 78 | } 79 | if result != nil { 80 | break 81 | } 82 | } 83 | return result 84 | } 85 | 86 | static func findSuperView(_ base: UIView) -> T? where T: UIView { 87 | var v: UIView? = base 88 | while v != nil { 89 | if let v = v?.superview as? T { 90 | return v 91 | } else { 92 | v = v?.superview 93 | } 94 | } 95 | return nil 96 | } 97 | 98 | static func findAllViews(_ root: UIView) -> [T] where T: UIView { 99 | var result: [T] = [] 100 | for v in root.subviews { 101 | if let r = v as? T { 102 | result.append(r) 103 | } 104 | let rv: [T] = findAllViews(v) 105 | result.append(contentsOf: rv) 106 | } 107 | return result 108 | } 109 | } 110 | 111 | extension UIView { 112 | 113 | public func first(where condition: @escaping (UIView) -> Bool) -> UIView? { 114 | return UISearchUtilities.first(inView: self, where: condition) 115 | } 116 | 117 | public func find() -> T? where T: UIView { 118 | return UISearchUtilities.findView(self) 119 | } 120 | 121 | public func findSuperView() -> T? where T: UIView { 122 | return UISearchUtilities.findSuperView(self) 123 | } 124 | 125 | public func findAll() -> [T] where T: UIView { 126 | return UISearchUtilities.findAllViews(self) 127 | } 128 | } 129 | 130 | 131 | extension UIView { 132 | 133 | public func findConstraints(attribute: NSLayoutConstraint.Attribute) -> [NSLayoutConstraint] { 134 | let result = constraints.filter { $0.firstAttribute == attribute && $0.firstItem as? NSObject == self } 135 | return result 136 | } 137 | 138 | public func findSuperviewConstraints(attribute: NSLayoutConstraint.Attribute) -> [NSLayoutConstraint] { 139 | let result = superview?.constraints.filter { 140 | ($0.firstAttribute == attribute && $0.firstItem as? NSObject == self) || 141 | ($0.secondAttribute == attribute && $0.secondItem as? NSObject == self) 142 | } 143 | return result ?? [] 144 | } 145 | 146 | public var allConstraints: [NSLayoutConstraint] { 147 | let selfconstraints = constraints.filter { $0.firstItem === self || $0.secondItem === self } 148 | let superviewConstraints = superview?.constraints.filter { $0.firstItem === self || $0.secondItem === self } ?? [] 149 | return (selfconstraints + superviewConstraints) 150 | } 151 | } 152 | 153 | extension UIView { 154 | @nonobjc @discardableResult 155 | 156 | public func insert(toSuperview superview: UIView, at: Int) -> Self { 157 | superview.insertSubview(self, at: at) 158 | return self 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /utopia/Source/UIKit/Keyboard/KeyboardObserver.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Keyboard.swift 3 | // Demo 4 | // 5 | // Created by MORITANAOKI on 2015/12/14. 6 | // Copyright © 2015年 molabo. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public enum KeyboardEventType { 12 | case willShow 13 | case didShow 14 | case willHide 15 | case didHide 16 | case willChangeFrame 17 | case didChangeFrame 18 | 19 | public var notificationName: NSNotification.Name { 20 | switch self { 21 | case .willShow: return UIResponder.keyboardWillShowNotification 22 | case .didShow: return UIResponder.keyboardDidShowNotification 23 | case .willHide: return UIResponder.keyboardWillHideNotification 24 | case .didHide: return UIResponder.keyboardDidHideNotification 25 | case .willChangeFrame: return UIResponder.keyboardWillChangeFrameNotification 26 | case .didChangeFrame: return UIResponder.keyboardDidChangeFrameNotification 27 | } 28 | } 29 | 30 | init?(name: NSNotification.Name) { 31 | switch name { 32 | case UIResponder.keyboardWillShowNotification: 33 | self = .willShow 34 | case UIResponder.keyboardDidShowNotification: 35 | self = .didShow 36 | case UIResponder.keyboardWillHideNotification: 37 | self = .willHide 38 | case UIResponder.keyboardDidHideNotification: 39 | self = .didHide 40 | case UIResponder.keyboardWillChangeFrameNotification: 41 | self = .willChangeFrame 42 | case UIResponder.keyboardDidChangeFrameNotification: 43 | self = .didChangeFrame 44 | default: 45 | return nil 46 | } 47 | } 48 | 49 | static func allEventNames() -> [NSNotification.Name] { 50 | return [ 51 | KeyboardEventType.willShow, 52 | KeyboardEventType.didShow, 53 | KeyboardEventType.willHide, 54 | KeyboardEventType.didHide, 55 | KeyboardEventType.willChangeFrame, 56 | KeyboardEventType.didChangeFrame 57 | ].map { $0.notificationName } 58 | } 59 | } 60 | 61 | public struct KeyboardEvent { 62 | public let type: KeyboardEventType 63 | public let keyboardFrameBegin: CGRect 64 | public let keyboardFrameEnd: CGRect 65 | public let curve: UIView.AnimationCurve 66 | public let duration: TimeInterval 67 | public let isLocal: Bool 68 | 69 | public var options: UIView.AnimationOptions { 70 | return UIView.AnimationOptions(rawValue: UInt(curve.rawValue << 16)) 71 | } 72 | 73 | init?(notification: Notification) { 74 | guard let userInfo = (notification as NSNotification).userInfo else { return nil } 75 | guard let type = KeyboardEventType(name: notification.name) else { return nil } 76 | guard let begin = (userInfo[UIResponder.keyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue else { return nil } 77 | guard let end = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue else { return nil } 78 | guard 79 | let curveInt = (userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as? NSNumber)?.intValue, 80 | let curve = UIView.AnimationCurve(rawValue: curveInt) 81 | else { return nil } 82 | guard 83 | let durationDouble = (userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber)?.doubleValue 84 | else { return nil } 85 | guard let isLocalInt = (userInfo[UIResponder.keyboardIsLocalUserInfoKey] as? NSNumber)?.intValue 86 | else { return nil } 87 | 88 | self.type = type 89 | self.keyboardFrameBegin = begin 90 | self.keyboardFrameEnd = end 91 | self.curve = curve 92 | self.duration = TimeInterval(durationDouble) 93 | self.isLocal = isLocalInt == 1 94 | } 95 | } 96 | 97 | public enum KeyboardState { 98 | case initial 99 | case showing 100 | case shown 101 | case hiding 102 | case hidden 103 | case changing 104 | } 105 | 106 | public typealias KeyboardEventClosure = ((_ event: KeyboardEvent) -> Void) 107 | 108 | open class KeyboardObserver { 109 | open var state = KeyboardState.initial 110 | open var isEnabled = true 111 | fileprivate var eventClosures = [KeyboardEventClosure]() 112 | 113 | deinit { 114 | eventClosures.removeAll() 115 | KeyboardEventType.allEventNames().forEach { 116 | NotificationCenter.default.removeObserver(self, name: $0, object: nil) 117 | } 118 | } 119 | 120 | public init() { 121 | KeyboardEventType.allEventNames().forEach { 122 | NotificationCenter.default.addObserver(self, selector: #selector(notified(_:)), name: $0, object: nil) 123 | } 124 | } 125 | 126 | open func observe(_ event: @escaping KeyboardEventClosure) { 127 | eventClosures.append(event) 128 | } 129 | } 130 | 131 | internal extension KeyboardObserver { 132 | @objc func notified(_ notification: Notification) { 133 | guard let event = KeyboardEvent(notification: notification) else { return } 134 | 135 | switch event.type { 136 | case .willShow: state = .showing 137 | case .didShow: state = .shown 138 | case .willHide: state = .hiding 139 | case .didHide: state = .hidden 140 | case .willChangeFrame: state = .changing 141 | case .didChangeFrame: state = .shown 142 | } 143 | 144 | if !isEnabled { return } 145 | eventClosures.forEach { $0(event) } 146 | } 147 | } 148 | 149 | -------------------------------------------------------------------------------- /utopia/Source/Transitions/InteractiveDismiss.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | protocol InteractiveDismiss: class { 5 | 6 | var canBeginInteractiveDismiss: Bool { get } 7 | var backgroundView: UIView { get } 8 | var contentView: UIView { get } 9 | 10 | func setupInteractiveDismission() 11 | func viewDidCompleteInteractiveDismiss() 12 | func viewPossiblyWillDissmiss() 13 | } 14 | 15 | private struct AssociatedKeys { 16 | static var exceededThreshold: Int = 0 17 | static var totalTranslation: Int = 1 18 | static var gestureDelegate: Int = 2 19 | } 20 | 21 | extension InteractiveDismiss where Self: UIViewController { 22 | 23 | private var exceededDismissionThreshold: Bool { 24 | get { return getAssociatedObject(&AssociatedKeys.exceededThreshold) ?? false } 25 | set { setAssociatedObject(&AssociatedKeys.exceededThreshold, newValue) } 26 | } 27 | 28 | private var totalTranslationY: CGFloat { 29 | get { return getAssociatedObject(&AssociatedKeys.totalTranslation) ?? 0 } 30 | set { setAssociatedObject(&AssociatedKeys.totalTranslation, newValue) } 31 | } 32 | 33 | private var gestureDelegate: GestureRecognizerDelegate? { 34 | get { return getAssociatedObject(&AssociatedKeys.gestureDelegate) } 35 | set { setAssociatedObject(&AssociatedKeys.gestureDelegate, newValue) } 36 | } 37 | 38 | func dismissAnimated() { 39 | animateExit() 40 | } 41 | 42 | func setupInteractiveDismission() { 43 | 44 | let panGesture = UIPanGestureRecognizer {[weak self] recognizer in 45 | self?.didPanView(panGesture: recognizer as! UIPanGestureRecognizer) 46 | } 47 | panGesture.cancelsTouchesInView = false 48 | let delegate = GestureRecognizerDelegate() 49 | delegate.recognizerShouldBegin = { [unowned self] _ in return self.canBeginInteractiveDismiss } 50 | panGesture.delegate = delegate 51 | gestureDelegate = delegate 52 | view.addGestureRecognizer(panGesture) 53 | } 54 | 55 | private func didPanView(panGesture: UIPanGestureRecognizer) { 56 | 57 | // Update translation 58 | let translation = panGesture.translation(in: view) 59 | 60 | // Check which view should update 61 | if contentView.frame.origin.y + translation.y > 0 { 62 | 63 | // We are scrolling down 64 | totalTranslationY += translation.y 65 | 66 | // Check threshold 67 | if totalTranslationY > Theme.scrollDownSlopThreshold { 68 | contentView.frame.origin.y += translation.y 69 | 70 | // Updated the view behind this with the proper index 71 | // Prevents a jitter when exiting 72 | if !exceededDismissionThreshold { 73 | exceededDismissionThreshold = true 74 | viewPossiblyWillDissmiss() 75 | } 76 | } else { 77 | contentView.frame.origin.y = 0 78 | } 79 | 80 | // Perform changes 81 | performTranslationBasedStyles() 82 | } 83 | 84 | // Handle extras 85 | switch panGesture.state { 86 | case .ended: 87 | if totalTranslationY > Theme.scrollDownSlopThreshold { 88 | 89 | // Determine velocity 90 | let velocity = panGesture.velocity(in: view) 91 | let projectedY = Inertia.project(initialVelocity: velocity.y) 92 | let duration = TimeInterval(view.frame.size.height / velocity.y) 93 | 94 | // Handle case 95 | if projectedY >= Theme.flingToDismissVelocityThreshold { 96 | animateExit(duration, projectedY) 97 | } else if contentView.frame.origin.y >= view.frame.height * 0.4 { 98 | animateExit() 99 | } else { 100 | let springVelocity = projectedY / view.frame.height 101 | animateBack(intensity: springVelocity) 102 | } 103 | } 104 | 105 | // Reset 106 | totalTranslationY = 0 107 | exceededDismissionThreshold = false 108 | default: break 109 | } 110 | 111 | // Reset 112 | panGesture.setTranslation(.zero, in: view) 113 | } 114 | 115 | private func animateExit(_ duration: TimeInterval = 0.33, _ projectedY: CGFloat = 0) { 116 | UIView.animate(withDuration: duration, animations: { 117 | self.contentView.frame.origin.y = self.view.frame.height + projectedY 118 | self.backgroundView.alpha = 0 119 | }) { _ in 120 | self.viewDidCompleteInteractiveDismiss() 121 | self.dismiss(animated: false, completion: nil) 122 | } 123 | } 124 | 125 | private func animateBack(intensity: CGFloat) { 126 | let velocity = intensity.limited(1, 1 + intensity) 127 | UIView.animate(withDuration: 0.33, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: velocity, options: .allowUserInteraction, animations: { 128 | self.contentView.frame.origin.y = 0 129 | self.backgroundView.alpha = 1 130 | }, completion: nil) 131 | } 132 | 133 | // Changes styles based on the drag og the content view 134 | private func performTranslationBasedStyles() { 135 | let translation = 1 - (contentView.frame.origin.y / view.frame.height) 136 | backgroundView.alpha = translation 137 | } 138 | } 139 | 140 | 141 | // MARK: - Theme 142 | private enum Theme { 143 | static let scrollDownSlopThreshold: CGFloat = 36 144 | static let flingToDismissVelocityThreshold: CGFloat = 150 145 | } 146 | -------------------------------------------------------------------------------- /utopia/Source/UIKit/Extensions/UIImage+ext.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | extension UIImage { 5 | 6 | public var original: UIImage { 7 | return withRenderingMode(.alwaysOriginal) 8 | } 9 | 10 | public var template: UIImage { 11 | return withRenderingMode(.alwaysTemplate) 12 | } 13 | 14 | public var aspectRatio: CGFloat { 15 | guard size.height > 0 else { return 1 } 16 | return size.width / size.height 17 | } 18 | 19 | /** 20 | Returns a copy of self, tinted by the given color. 21 | 22 | - parameter tintColor: The UIColor to tint by. 23 | - returns: A copy of self, tinted by the tintColor. 24 | */ 25 | public func tinted(_ tintColor: UIColor) -> UIImage { 26 | guard let cgImage = cgImage else { return self } 27 | 28 | UIGraphicsBeginImageContextWithOptions(size, false, UIScreen.main.scale) 29 | 30 | defer { UIGraphicsEndImageContext() } 31 | 32 | let context = UIGraphicsGetCurrentContext() 33 | context?.saveGState() 34 | 35 | tintColor.setFill() 36 | 37 | context?.translateBy(x: 0, y: size.height) 38 | context?.scaleBy(x: 1, y: -1) 39 | context?.setBlendMode(.normal) 40 | 41 | let rect = CGRect(origin: .zero, size: size) 42 | context?.draw(cgImage, in: rect) 43 | 44 | context?.clip(to: rect, mask: cgImage) 45 | context?.addRect(rect) 46 | context?.drawPath(using: .fill) 47 | context?.restoreGState() 48 | 49 | return UIGraphicsGetImageFromCurrentImageContext() ?? self 50 | } 51 | 52 | /** 53 | Returns a copy of self, scaled by a specified scale factor (with an optional image orientation). 54 | 55 | Example: 56 | ``` 57 | let image = UIImage(named: ) // image size = (width: 200, height: 100) 58 | image?.resized(width: 50, height: 50) // image size = (width: 50, height: 50) 59 | image?.resized(width: 50, height: 50, maintainAspectRatio: true) // image size = (width: 50, height: 25) 60 | ``` 61 | 62 | - parameter width: The width to which to resize the image. 63 | - parameter height: The height to which to resize the image. 64 | - parameter maintainAspectRatio: A Boolean flag indicating whether or not to maintain the image's aspect ratio when resizing (optional, defaults to `false`). 65 | - returns: A copy of self, resized to a specified width and heigh (with an option to maintain the original aspect ratio). 66 | */ 67 | public func resized(width: CGFloat, height: CGFloat, maintainAspectRatio: Bool = false, insets: UIEdgeInsets = .zero) -> UIImage? { 68 | 69 | let inputSize = CGSize(width: width, height: height) 70 | let newSize: CGSize 71 | 72 | if maintainAspectRatio { 73 | let ratio = min(inputSize.width / size.width, inputSize.height / size.height) 74 | newSize = CGSize(width: size.width * ratio, height: size.height * ratio) 75 | } 76 | else { 77 | newSize = inputSize 78 | } 79 | 80 | let contextSize = CGSize(width: newSize.width + insets.right + insets.left, height: newSize.height + insets.bottom + insets.top) 81 | UIGraphicsBeginImageContextWithOptions(contextSize, false, UIScreen.main.scale) 82 | 83 | defer { UIGraphicsEndImageContext() } 84 | 85 | draw(in: CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: newSize)) 86 | 87 | return UIGraphicsGetImageFromCurrentImageContext() 88 | } 89 | 90 | public func resized(size: CGSize, maintainAspectRatio: Bool = false, insets: UIEdgeInsets = .zero) -> UIImage? { 91 | return self.resized(width: size.width, height: size.height, maintainAspectRatio: maintainAspectRatio, insets: insets) 92 | } 93 | 94 | 95 | /** 96 | Returns a copy of self, scaled by a specified scale factor (with an optional image orientation). 97 | 98 | Example: 99 | ``` 100 | let image = UIImage(named: ) // image size = (width: 200, height: 100) 101 | image?.scaled(by: 0.25) // image size = (width: 50, height: 25) 102 | image?.scaled(by: 2) // image size = (width: 400, height: 200) 103 | ``` 104 | 105 | - parameter scaleFactor: The factor at which to scale this image. 106 | - parameter orientiation: The orientation to use for the scaled image (optional, defaults to the image's `imageOrientation` property). 107 | - returns: A copy of self, scaled by the scaleFactor (with an optional image orientation). 108 | */ 109 | public func scaled(by scaleFactor: CGFloat, withOrientation orientation: UIImage.Orientation? = nil) -> UIImage? { 110 | guard let coreImage = cgImage else { return nil } 111 | 112 | return UIImage(cgImage: coreImage, scale: 1/scaleFactor, orientation: orientation ?? imageOrientation) 113 | } 114 | 115 | /** 116 | Returns an optional image using the `drawingCommands` 117 | 118 | Example: 119 | 120 | let image = UIImage(size: CGSize(width: 12, height: 24)) { 121 | let path = UIBezierPath() 122 | path.move(to: ...) 123 | path.addLine(to: ...) 124 | path.addLine(to: ...) 125 | path.lineWidth = 2 126 | UIColor.white.setStroke() 127 | path.stroke() 128 | } 129 | 130 | - parameter size: The image size. 131 | - parameter drawingCommands: A closure describing the path, fill/stroke color, etc. 132 | - returns: An optional image based on `drawingCommands`. 133 | */ 134 | public convenience init?(size: CGSize, drawingCommands commands: () -> Void) { 135 | UIGraphicsBeginImageContextWithOptions(size, false, 0) 136 | commands() 137 | 138 | defer { UIGraphicsEndImageContext() } 139 | 140 | guard let image = UIGraphicsGetImageFromCurrentImageContext()?.cgImage else { return nil } 141 | 142 | self.init(cgImage: image) 143 | } 144 | 145 | /** 146 | Returns an optional image using the specified color 147 | 148 | - parameter color: The color of the image. 149 | - returns: An optional image based on `color`. 150 | */ 151 | public convenience init?(color: UIColor) { 152 | let size = CGSize(width: 1, height: 1) 153 | let rect = CGRect(origin: .zero, size: size) 154 | UIGraphicsBeginImageContextWithOptions(size, false, 0) 155 | 156 | color.setFill() 157 | UIRectFill(rect) 158 | 159 | defer { UIGraphicsEndImageContext() } 160 | 161 | guard let image = UIGraphicsGetImageFromCurrentImageContext()?.cgImage else { return nil } 162 | 163 | self.init(cgImage: image) 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-utopia_Example/Pods-utopia_Example-resources.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | set -u 4 | set -o pipefail 5 | 6 | if [ -z ${UNLOCALIZED_RESOURCES_FOLDER_PATH+x} ]; then 7 | # If UNLOCALIZED_RESOURCES_FOLDER_PATH is not set, then there's nowhere for us to copy 8 | # resources to, so exit 0 (signalling the script phase was successful). 9 | exit 0 10 | fi 11 | 12 | mkdir -p "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 13 | 14 | RESOURCES_TO_COPY=${PODS_ROOT}/resources-to-copy-${TARGETNAME}.txt 15 | > "$RESOURCES_TO_COPY" 16 | 17 | XCASSET_FILES=() 18 | 19 | # This protects against multiple targets copying the same framework dependency at the same time. The solution 20 | # was originally proposed here: https://lists.samba.org/archive/rsync/2008-February/020158.html 21 | RSYNC_PROTECT_TMP_FILES=(--filter "P .*.??????") 22 | 23 | case "${TARGETED_DEVICE_FAMILY:-}" in 24 | 1,2) 25 | TARGET_DEVICE_ARGS="--target-device ipad --target-device iphone" 26 | ;; 27 | 1) 28 | TARGET_DEVICE_ARGS="--target-device iphone" 29 | ;; 30 | 2) 31 | TARGET_DEVICE_ARGS="--target-device ipad" 32 | ;; 33 | 3) 34 | TARGET_DEVICE_ARGS="--target-device tv" 35 | ;; 36 | 4) 37 | TARGET_DEVICE_ARGS="--target-device watch" 38 | ;; 39 | *) 40 | TARGET_DEVICE_ARGS="--target-device mac" 41 | ;; 42 | esac 43 | 44 | install_resource() 45 | { 46 | if [[ "$1" = /* ]] ; then 47 | RESOURCE_PATH="$1" 48 | else 49 | RESOURCE_PATH="${PODS_ROOT}/$1" 50 | fi 51 | if [[ ! -e "$RESOURCE_PATH" ]] ; then 52 | cat << EOM 53 | error: Resource "$RESOURCE_PATH" not found. Run 'pod install' to update the copy resources script. 54 | EOM 55 | exit 1 56 | fi 57 | case $RESOURCE_PATH in 58 | *.storyboard) 59 | echo "ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile ${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .storyboard`.storyboardc $RESOURCE_PATH --sdk ${SDKROOT} ${TARGET_DEVICE_ARGS}" || true 60 | ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .storyboard`.storyboardc" "$RESOURCE_PATH" --sdk "${SDKROOT}" ${TARGET_DEVICE_ARGS} 61 | ;; 62 | *.xib) 63 | echo "ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile ${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .xib`.nib $RESOURCE_PATH --sdk ${SDKROOT} ${TARGET_DEVICE_ARGS}" || true 64 | ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .xib`.nib" "$RESOURCE_PATH" --sdk "${SDKROOT}" ${TARGET_DEVICE_ARGS} 65 | ;; 66 | *.framework) 67 | echo "mkdir -p ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" || true 68 | mkdir -p "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 69 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" $RESOURCE_PATH ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" || true 70 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 71 | ;; 72 | *.xcdatamodel) 73 | echo "xcrun momc \"$RESOURCE_PATH\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH"`.mom\"" || true 74 | xcrun momc "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcdatamodel`.mom" 75 | ;; 76 | *.xcdatamodeld) 77 | echo "xcrun momc \"$RESOURCE_PATH\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcdatamodeld`.momd\"" || true 78 | xcrun momc "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcdatamodeld`.momd" 79 | ;; 80 | *.xcmappingmodel) 81 | echo "xcrun mapc \"$RESOURCE_PATH\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcmappingmodel`.cdm\"" || true 82 | xcrun mapc "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcmappingmodel`.cdm" 83 | ;; 84 | *.xcassets) 85 | ABSOLUTE_XCASSET_FILE="$RESOURCE_PATH" 86 | XCASSET_FILES+=("$ABSOLUTE_XCASSET_FILE") 87 | ;; 88 | *) 89 | echo "$RESOURCE_PATH" || true 90 | echo "$RESOURCE_PATH" >> "$RESOURCES_TO_COPY" 91 | ;; 92 | esac 93 | } 94 | 95 | mkdir -p "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 96 | rsync -avr --copy-links --no-relative --exclude '*/.svn/*' --files-from="$RESOURCES_TO_COPY" / "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 97 | if [[ "${ACTION}" == "install" ]] && [[ "${SKIP_INSTALL}" == "NO" ]]; then 98 | mkdir -p "${INSTALL_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 99 | rsync -avr --copy-links --no-relative --exclude '*/.svn/*' --files-from="$RESOURCES_TO_COPY" / "${INSTALL_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 100 | fi 101 | rm -f "$RESOURCES_TO_COPY" 102 | 103 | if [[ -n "${WRAPPER_EXTENSION}" ]] && [ "`xcrun --find actool`" ] && [ -n "${XCASSET_FILES:-}" ] 104 | then 105 | # Find all other xcassets (this unfortunately includes those of path pods and other targets). 106 | OTHER_XCASSETS=$(find "$PWD" -iname "*.xcassets" -type d) 107 | while read line; do 108 | if [[ $line != "${PODS_ROOT}*" ]]; then 109 | XCASSET_FILES+=("$line") 110 | fi 111 | done <<<"$OTHER_XCASSETS" 112 | 113 | if [ -z ${ASSETCATALOG_COMPILER_APPICON_NAME+x} ]; then 114 | printf "%s\0" "${XCASSET_FILES[@]}" | xargs -0 xcrun actool --output-format human-readable-text --notices --warnings --platform "${PLATFORM_NAME}" --minimum-deployment-target "${!DEPLOYMENT_TARGET_SETTING_NAME}" ${TARGET_DEVICE_ARGS} --compress-pngs --compile "${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 115 | else 116 | printf "%s\0" "${XCASSET_FILES[@]}" | xargs -0 xcrun actool --output-format human-readable-text --notices --warnings --platform "${PLATFORM_NAME}" --minimum-deployment-target "${!DEPLOYMENT_TARGET_SETTING_NAME}" ${TARGET_DEVICE_ARGS} --compress-pngs --compile "${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" --app-icon "${ASSETCATALOG_COMPILER_APPICON_NAME}" --output-partial-info-plist "${TARGET_TEMP_DIR}/assetcatalog_generated_info_cocoapods.plist" 117 | fi 118 | fi 119 | -------------------------------------------------------------------------------- /utopia/Source/Signals/UIControl+Signals.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2014 - 2017 Tuomas Artman. All rights reserved. 3 | // 4 | 5 | #if os(iOS) || os(tvOS) 6 | 7 | import UIKit 8 | 9 | /// Extends UIControl with signals for all ui control events. 10 | public extension UIControl { 11 | /// A signal that fires for each touch down control event. 12 | var onTouchDown: Signal<(Void)> { 13 | return getOrCreateSignalForUIControlEvent(.touchDown); 14 | } 15 | 16 | /// A signal that fires for each touch down repeat control event. 17 | var onTouchDownRepeat: Signal<(Void)> { 18 | return getOrCreateSignalForUIControlEvent(.touchDownRepeat); 19 | } 20 | 21 | /// A signal that fires for each touch drag inside control event. 22 | var onTouchDragInside: Signal<(Void)> { 23 | return getOrCreateSignalForUIControlEvent(.touchDragInside); 24 | } 25 | 26 | /// A signal that fires for each touch drag outside control event. 27 | var onTouchDragOutside: Signal<(Void)> { 28 | return getOrCreateSignalForUIControlEvent(.touchDragOutside); 29 | } 30 | 31 | /// A signal that fires for each touch drag enter control event. 32 | var onTouchDragEnter: Signal<(Void)> { 33 | return getOrCreateSignalForUIControlEvent(.touchDragEnter); 34 | } 35 | 36 | /// A signal that fires for each touch drag exit control event. 37 | var onTouchDragExit: Signal<(Void)> { 38 | return getOrCreateSignalForUIControlEvent(.touchDragExit); 39 | } 40 | 41 | /// A signal that fires for each touch up inside control event. 42 | var onTouchUpInside: Signal<(Void)> { 43 | return getOrCreateSignalForUIControlEvent(.touchUpInside); 44 | } 45 | 46 | /// A signal that fires for each primary action control event. 47 | var onPrimaryActionTriggered: Signal<(Void)> { 48 | return getOrCreateSignalForUIControlEvent(.primaryActionTriggered); 49 | } 50 | 51 | /// A signal that fires for each touch up outside control event. 52 | var onTouchUpOutside: Signal<(Void)> { 53 | return getOrCreateSignalForUIControlEvent(.touchUpOutside); 54 | } 55 | 56 | /// A signal that fires for each touch cancel control event. 57 | var onTouchCancel: Signal<(Void)> { 58 | return getOrCreateSignalForUIControlEvent(.touchCancel); 59 | } 60 | 61 | /// A signal that fires for each value changed control event. 62 | var onValueChanged: Signal<(Void)> { 63 | return getOrCreateSignalForUIControlEvent(.valueChanged); 64 | } 65 | 66 | /// A signal that fires for each editing did begin control event. 67 | var onEditingDidBegin: Signal<(Void)> { 68 | return getOrCreateSignalForUIControlEvent(.editingDidBegin); 69 | } 70 | 71 | /// A signal that fires for each editing changed control event. 72 | var onEditingChanged: Signal<(Void)> { 73 | return getOrCreateSignalForUIControlEvent(.editingChanged); 74 | } 75 | 76 | /// A signal that fires for each editing did end control event. 77 | var onEditingDidEnd: Signal<(Void)> { 78 | return getOrCreateSignalForUIControlEvent(.editingDidEnd); 79 | } 80 | 81 | /// A signal that fires for each editing did end on exit control event. 82 | var onEditingDidEndOnExit: Signal<(Void)> { 83 | return getOrCreateSignalForUIControlEvent(.editingDidEndOnExit); 84 | } 85 | 86 | // MARK: - Private interface 87 | 88 | private struct AssociatedKeys { 89 | static var SignalDictionaryKey = "signals_signalKey" 90 | } 91 | 92 | private static let eventToKey: [UIControl.Event: NSString] = [ 93 | .touchDown: "TouchDown", 94 | .touchDownRepeat: "TouchDownRepeat", 95 | .touchDragInside: "TouchDragInside", 96 | .touchDragOutside: "TouchDragOutside", 97 | .touchDragEnter: "TouchDragEnter", 98 | .touchDragExit: "TouchDragExit", 99 | .touchUpInside: "TouchUpInside", 100 | .primaryActionTriggered: "PrimaryActionTriggered", 101 | .touchUpOutside: "TouchUpOutside", 102 | .touchCancel: "TouchCancel", 103 | .valueChanged: "ValueChanged", 104 | .editingDidBegin: "EditingDidBegin", 105 | .editingChanged: "EditingChanged", 106 | .editingDidEnd: "EditingDidEnd", 107 | .editingDidEndOnExit: "EditingDidEndOnExit"] 108 | 109 | private func getOrCreateSignalForUIControlEvent(_ event: UIControl.Event) -> Signal<(Void)> { 110 | guard let key = UIControl.eventToKey[event] else { 111 | assertionFailure("Event type is not handled") 112 | return Signal() 113 | } 114 | let dictionary = getOrCreateAssociatedObject(self, associativeKey: &AssociatedKeys.SignalDictionaryKey, defaultValue: NSMutableDictionary(), policy: objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) 115 | 116 | if let signal = dictionary[key] as? Signal<()> { 117 | return signal 118 | } else { 119 | let signal = Signal<()>() 120 | dictionary[key] = signal 121 | self.addTarget(self, action: Selector("eventHandler\(key)"), for: event) 122 | return signal 123 | } 124 | } 125 | 126 | private func handleUIControlEvent(_ uiControlEvent: UIControl.Event) { 127 | getOrCreateSignalForUIControlEvent(uiControlEvent).fire(()) 128 | } 129 | 130 | @objc private dynamic func eventHandlerTouchDown() { 131 | handleUIControlEvent(.touchDown) 132 | } 133 | 134 | @objc private dynamic func eventHandlerTouchDownRepeat() { 135 | handleUIControlEvent(.touchDownRepeat) 136 | } 137 | 138 | @objc private dynamic func eventHandlerTouchDragInside() { 139 | handleUIControlEvent(.touchDragInside) 140 | } 141 | 142 | @objc private dynamic func eventHandlerTouchDragOutside() { 143 | handleUIControlEvent(.touchDragOutside) 144 | } 145 | 146 | @objc private dynamic func eventHandlerTouchDragEnter() { 147 | handleUIControlEvent(.touchDragEnter) 148 | } 149 | 150 | @objc private dynamic func eventHandlerTouchDragExit() { 151 | handleUIControlEvent(.touchDragExit) 152 | } 153 | 154 | @objc private dynamic func eventHandlerTouchUpInside() { 155 | handleUIControlEvent(.touchUpInside) 156 | } 157 | 158 | @objc private dynamic func eventHandlerPrimaryActionTriggered() { 159 | handleUIControlEvent(.primaryActionTriggered) 160 | } 161 | 162 | @objc private dynamic func eventHandlerTouchUpOutside() { 163 | handleUIControlEvent(.touchUpOutside) 164 | } 165 | 166 | @objc private dynamic func eventHandlerTouchCancel() { 167 | handleUIControlEvent(.touchCancel) 168 | } 169 | 170 | @objc private dynamic func eventHandlerValueChanged() { 171 | handleUIControlEvent(.valueChanged) 172 | } 173 | 174 | @objc private dynamic func eventHandlerEditingDidBegin() { 175 | handleUIControlEvent(.editingDidBegin) 176 | } 177 | 178 | @objc private dynamic func eventHandlerEditingChanged() { 179 | handleUIControlEvent(.editingChanged) 180 | } 181 | 182 | @objc private dynamic func eventHandlerEditingDidEnd() { 183 | handleUIControlEvent(.editingDidEnd) 184 | } 185 | 186 | @objc private dynamic func eventHandlerEditingDidEndOnExit() { 187 | handleUIControlEvent(.editingDidEndOnExit) 188 | } 189 | } 190 | 191 | extension UIControl.Event: Hashable { 192 | public var hashValue: Int { 193 | return Int(self.rawValue) 194 | } 195 | } 196 | 197 | #endif 198 | -------------------------------------------------------------------------------- /utopia/Source/Signals/Signal.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2014 - 2017 Tuomas Artman. All rights reserved. 3 | // 4 | 5 | import Foundation 6 | 7 | /// Create instances of `Signal` and assign them to public constants on your class for each event type that your 8 | /// class fires. 9 | final public class Signal { 10 | 11 | public typealias SignalCallback = (T) -> Void 12 | 13 | /// Whether or not the `Signal` should retain a reference to the last data it was fired with. Defaults to false. 14 | public var retainLastData: Bool = false { 15 | didSet { 16 | if !retainLastData { 17 | lastDataFired = nil 18 | } 19 | } 20 | } 21 | 22 | /// The last data that the `Signal` was fired with. In order for the `Signal` to retain the last fired data, its 23 | /// `retainLastFired`-property needs to be set to true 24 | public private(set) var lastDataFired: T? = nil 25 | 26 | /// All the observers of to the `Signal`. 27 | public var observers: [AnyObject] { 28 | return signalListeners.compactMap { $0.observer } 29 | } 30 | 31 | private var signalListeners = [SignalSubscription]() 32 | 33 | 34 | // MARK: - Init 35 | 36 | /// Initializer. 37 | /// 38 | /// - parameter retainLastData: Whether or not the Signal should retain a reference to the last data it was fired 39 | /// with. Defaults to false. 40 | public init(retainLastData: Bool = false) { 41 | self.retainLastData = retainLastData 42 | } 43 | 44 | 45 | // MARK: - Subscribe 46 | 47 | /// Subscribes an observer to the `Signal`. 48 | /// 49 | /// - parameter observer: The observer that subscribes to the `Signal`. Should the observer be deallocated, the 50 | /// subscription is automatically cancelled. 51 | /// - parameter callback: The closure to invoke whenever the `Signal` fires. 52 | /// - returns: A `SignalSubscription` that can be used to cancel or filter the subscription. 53 | @discardableResult 54 | public func subscribe(with observer: AnyObject, callback: @escaping SignalCallback) -> SignalSubscription { 55 | flushCancelledListeners() 56 | let signalListener = SignalSubscription(observer: observer, callback: callback); 57 | signalListeners.append(signalListener) 58 | return signalListener 59 | } 60 | 61 | 62 | /// Subscribes an observer to the `Signal`. The subscription is automatically canceled after the `Signal` has 63 | /// fired once. 64 | /// 65 | /// - parameter observer: The observer that subscribes to the `Signal`. Should the observer be deallocated, the 66 | /// subscription is automatically cancelled. 67 | /// - parameter callback: The closure to invoke when the signal fires for the first time. 68 | @discardableResult 69 | public func subscribeOnce(with observer: AnyObject, callback: @escaping SignalCallback) -> SignalSubscription { 70 | let signalListener = self.subscribe(with: observer, callback: callback) 71 | signalListener.once = true 72 | return signalListener 73 | } 74 | 75 | /// Subscribes an observer to the `Signal` and invokes its callback immediately with the last data fired by the 76 | /// `Signal` if it has fired at least once and if the `retainLastData` property has been set to true. 77 | /// 78 | /// - parameter observer: The observer that subscribes to the `Signal`. Should the observer be deallocated, the 79 | /// subscription is automatically cancelled. 80 | /// - parameter callback: The closure to invoke whenever the `Signal` fires. 81 | @discardableResult 82 | public func subscribePast(with observer: AnyObject, callback: @escaping SignalCallback) -> SignalSubscription { 83 | #if DEBUG 84 | signalsAssert(retainLastData, "can't subscribe to past events on Signal with retainLastData set to false") 85 | #endif 86 | let signalListener = self.subscribe(with: observer, callback: callback) 87 | if let lastDataFired = lastDataFired { 88 | callback(lastDataFired) 89 | } 90 | return signalListener 91 | } 92 | 93 | /// Subscribes an observer to the `Signal` and invokes its callback immediately with the last data fired by the 94 | /// `Signal` if it has fired at least once and if the `retainLastData` property has been set to true. If it has 95 | /// not been fired yet, it will continue listening until it fires for the first time. 96 | /// 97 | /// - parameter observer: The observer that subscribes to the `Signal`. Should the observer be deallocated, the 98 | /// subscription is automatically cancelled. 99 | /// - parameter callback: The closure to invoke whenever the signal fires. 100 | @discardableResult 101 | public func subscribePastOnce(with observer: AnyObject, callback: @escaping SignalCallback) -> SignalSubscription { 102 | #if DEBUG 103 | signalsAssert(retainLastData, "can't subscribe to past events on Signal with retainLastData set to false") 104 | #endif 105 | let signalListener = self.subscribe(with: observer, callback: callback) 106 | if let lastDataFired = lastDataFired { 107 | callback(lastDataFired) 108 | signalListener.cancel() 109 | } else { 110 | signalListener.once = true 111 | } 112 | return signalListener 113 | } 114 | 115 | 116 | // MARK: - Emit events 117 | 118 | /// Fires the `Singal`. 119 | /// 120 | /// - parameter data: The data to fire the `Signal` with. 121 | public func fire(_ data: T) { 122 | lastDataFired = retainLastData ? data : nil 123 | flushCancelledListeners() 124 | 125 | for signalListener in signalListeners { 126 | if signalListener.filter == nil || signalListener.filter!(data) == true { 127 | _ = signalListener.dispatch(data: data) 128 | } 129 | } 130 | } 131 | 132 | 133 | // MARK: - Cancel 134 | 135 | /// Cancels all subscriptions for an observer. 136 | /// 137 | /// - parameter observer: The observer whose subscriptions to cancel 138 | public func cancelSubscription(for observer: AnyObject) { 139 | signalListeners = signalListeners.filter { 140 | if let definiteListener:AnyObject = $0.observer { 141 | return definiteListener !== observer 142 | } 143 | return false 144 | } 145 | } 146 | 147 | /// Cancels all subscriptions for the `Signal`. 148 | public func cancelAllSubscriptions() { 149 | signalListeners.removeAll(keepingCapacity: false) 150 | } 151 | 152 | /// Clears the last fired data from the `Signal` and resets the fire count. 153 | public func clearLastData() { 154 | lastDataFired = nil 155 | } 156 | 157 | // MARK: - Private Interface 158 | 159 | private func flushCancelledListeners() { 160 | var removeListeners = false 161 | for signalListener in signalListeners { 162 | if signalListener.observer == nil { 163 | removeListeners = true 164 | } 165 | } 166 | if removeListeners { 167 | signalListeners = signalListeners.filter { 168 | return $0.observer != nil 169 | } 170 | } 171 | } 172 | } 173 | 174 | fileprivate func signalsAssert(_ condition: Bool, _ message: String) { 175 | #if DEBUG 176 | if let assertionHandlerOverride = assertionHandlerOverride { 177 | assertionHandlerOverride(condition, message) 178 | return 179 | } 180 | #endif 181 | assert(condition, message) 182 | } 183 | 184 | #if DEBUG 185 | var assertionHandlerOverride:((_ condition: Bool, _ message: String) -> ())? 186 | #endif 187 | -------------------------------------------------------------------------------- /utopia/Source/UIKit/Views/GradientView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | /// Simple view for drawing gradients and borders. 4 | @IBDesignable open class GradientView: UIView { 5 | 6 | // MARK: - Types 7 | /// The mode of the gradient. 8 | @objc public enum Mode: Int { 9 | /// A linear gradient. 10 | case linear 11 | 12 | /// A radial gradient. 13 | case radial 14 | } 15 | 16 | 17 | /// The direction of the gradient. 18 | @objc public enum Direction: Int { 19 | /// The gradient is vertical. 20 | case vertical 21 | 22 | /// The gradient is horizontal 23 | case horizontal 24 | } 25 | 26 | 27 | // MARK: - Properties 28 | /// An optional array of `UIColor` objects used to draw the gradient. If the value is `nil`, the `backgroundColor` 29 | /// will be drawn instead of a gradient. The default is `nil`. 30 | open var colors: [UIColor]? { 31 | didSet { 32 | updateGradient() 33 | } 34 | } 35 | 36 | /// An array of `UIColor` objects used to draw the dimmed gradient. If the value is `nil`, `colors` will be 37 | /// converted to grayscale. This will use the same `locations` as `colors`. If length of arrays don't match, bad 38 | /// things will happen. You must make sure the number of dimmed colors equals the number of regular colors. 39 | /// 40 | /// The default is `nil`. 41 | open var dimmedColors: [UIColor]? { 42 | didSet { 43 | updateGradient() 44 | } 45 | } 46 | 47 | /// Automatically dim gradient colors when prompted by the system (i.e. when an alert is shown). 48 | /// 49 | /// The default is `true`. 50 | open var automaticallyDims: Bool = true 51 | 52 | /// An optional array of `CGFloat`s defining the location of each gradient stop. 53 | /// 54 | /// The gradient stops are specified as values between `0` and `1`. The values must be monotonically increasing. If 55 | /// `nil`, the stops are spread uniformly across the range. 56 | /// 57 | /// Defaults to `nil`. 58 | open var locations: [CGFloat]? { 59 | didSet { 60 | updateGradient() 61 | } 62 | } 63 | 64 | /// The mode of the gradient. The default is `.Linear`. 65 | @IBInspectable open var mode: Mode = .linear { 66 | didSet { 67 | setNeedsDisplay() 68 | } 69 | } 70 | 71 | /// The direction of the gradient. Only valid for the `Mode.Linear` mode. The default is `.Vertical`. 72 | @IBInspectable open var direction: Direction = .vertical { 73 | didSet { 74 | setNeedsDisplay() 75 | } 76 | } 77 | 78 | /// 1px borders will be drawn instead of 1pt borders. The default is `false`. 79 | @IBInspectable open var drawsThinBorders: Bool = false { 80 | didSet { 81 | setNeedsDisplay() 82 | } 83 | } 84 | 85 | /// The top border color. The default is `nil`. 86 | @IBInspectable open var topBorderColor: UIColor? { 87 | didSet { 88 | setNeedsDisplay() 89 | } 90 | } 91 | 92 | /// The right border color. The default is `nil`. 93 | @IBInspectable open var rightBorderColor: UIColor? { 94 | didSet { 95 | setNeedsDisplay() 96 | } 97 | } 98 | 99 | /// The bottom border color. The default is `nil`. 100 | @IBInspectable open var bottomBorderColor: UIColor? { 101 | didSet { 102 | setNeedsDisplay() 103 | } 104 | } 105 | 106 | /// The left border color. The default is `nil`. 107 | @IBInspectable open var leftBorderColor: UIColor? { 108 | didSet { 109 | setNeedsDisplay() 110 | } 111 | } 112 | 113 | public override init(frame: CGRect) { 114 | super.init(frame: frame) 115 | setup() 116 | } 117 | 118 | required public init?(coder aDecoder: NSCoder) { 119 | super.init(coder: aDecoder) 120 | setup() 121 | } 122 | 123 | private func setup() { 124 | backgroundColor = .clear 125 | } 126 | 127 | // MARK: - UIView 128 | override open func draw(_ rect: CGRect) { 129 | let context = UIGraphicsGetCurrentContext() 130 | let size = bounds.size 131 | 132 | // Gradient 133 | if let gradient = gradient { 134 | let options: CGGradientDrawingOptions = [.drawsAfterEndLocation] 135 | 136 | if mode == .linear { 137 | let startPoint = CGPoint.zero 138 | let endPoint = direction == .vertical ? CGPoint(x: 0, y: size.height) : CGPoint(x: size.width, y: 0) 139 | context?.drawLinearGradient(gradient, start: startPoint, end: endPoint, options: options) 140 | } else { 141 | let center = CGPoint(x: bounds.midX, y: bounds.midY) 142 | context?.drawRadialGradient(gradient, startCenter: center, startRadius: 0, endCenter: center, endRadius: min(size.width, size.height) / 2, options: options) 143 | } 144 | } 145 | 146 | let screen: UIScreen = window?.screen ?? UIScreen.main 147 | let borderWidth: CGFloat = drawsThinBorders ? 1.0 / screen.scale : 0.0 148 | 149 | // Top border 150 | if let color = topBorderColor { 151 | context?.setFillColor(color.cgColor) 152 | context?.fill(CGRect(x: 0, y: 0, width: size.width, height: borderWidth)) 153 | } 154 | 155 | let sideY: CGFloat = topBorderColor != nil ? borderWidth : 0 156 | let sideHeight: CGFloat = size.height - sideY - (bottomBorderColor != nil ? borderWidth : 0) 157 | 158 | // Right border 159 | if let color = rightBorderColor { 160 | context?.setFillColor(color.cgColor) 161 | context?.fill(CGRect(x: size.width - borderWidth, y: sideY, width: borderWidth, height: sideHeight)) 162 | } 163 | 164 | // Bottom border 165 | if let color = bottomBorderColor { 166 | context?.setFillColor(color.cgColor) 167 | context?.fill(CGRect(x: 0, y: size.height - borderWidth, width: size.width, height: borderWidth)) 168 | } 169 | 170 | // Left border 171 | if let color = leftBorderColor { 172 | context?.setFillColor(color.cgColor) 173 | context?.fill(CGRect(x: 0, y: sideY, width: borderWidth, height: sideHeight)) 174 | } 175 | } 176 | 177 | override open func tintColorDidChange() { 178 | super.tintColorDidChange() 179 | 180 | if automaticallyDims { 181 | updateGradient() 182 | } 183 | } 184 | 185 | override open func didMoveToWindow() { 186 | super.didMoveToWindow() 187 | contentMode = .redraw 188 | } 189 | 190 | 191 | // MARK: - Private 192 | fileprivate var gradient: CGGradient? 193 | 194 | fileprivate func updateGradient() { 195 | gradient = nil 196 | setNeedsDisplay() 197 | 198 | let colors = gradientColors() 199 | if let colors = colors { 200 | let colorSpace = CGColorSpaceCreateDeviceRGB() 201 | let colorSpaceModel = colorSpace.model 202 | 203 | let gradientColors = colors.map { (color: UIColor) -> AnyObject in 204 | let cgColor = color.cgColor 205 | let cgColorSpace = cgColor.colorSpace ?? colorSpace 206 | 207 | // The color's color space is RGB, simply add it. 208 | if cgColorSpace.model == colorSpaceModel { 209 | return cgColor as AnyObject 210 | } 211 | 212 | // Convert to RGB. There may be a more efficient way to do this. 213 | var red: CGFloat = 0 214 | var blue: CGFloat = 0 215 | var green: CGFloat = 0 216 | var alpha: CGFloat = 0 217 | color.getRed(&red, green: &green, blue: &blue, alpha: &alpha) 218 | return UIColor(red: red, green: green, blue: blue, alpha: alpha).cgColor as AnyObject 219 | } as NSArray 220 | 221 | gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: locations) 222 | } 223 | } 224 | 225 | fileprivate func gradientColors() -> [UIColor]? { 226 | if tintAdjustmentMode == .dimmed { 227 | if let dimmedColors = dimmedColors { 228 | return dimmedColors 229 | } 230 | 231 | if automaticallyDims { 232 | if let colors = colors { 233 | return colors.map { 234 | var hue: CGFloat = 0 235 | var brightness: CGFloat = 0 236 | var alpha: CGFloat = 0 237 | 238 | $0.getHue(&hue, saturation: nil, brightness: &brightness, alpha: &alpha) 239 | 240 | return UIColor(hue: hue, saturation: 0, brightness: brightness, alpha: alpha) 241 | } 242 | } 243 | } 244 | } 245 | 246 | return colors 247 | } 248 | } 249 | 250 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-utopia_Example/Pods-utopia_Example-frameworks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | set -u 4 | set -o pipefail 5 | 6 | function on_error { 7 | echo "$(realpath -mq "${0}"):$1: error: Unexpected failure" 8 | } 9 | trap 'on_error $LINENO' ERR 10 | 11 | if [ -z ${FRAMEWORKS_FOLDER_PATH+x} ]; then 12 | # If FRAMEWORKS_FOLDER_PATH is not set, then there's nowhere for us to copy 13 | # frameworks to, so exit 0 (signalling the script phase was successful). 14 | exit 0 15 | fi 16 | 17 | echo "mkdir -p ${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 18 | mkdir -p "${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 19 | 20 | COCOAPODS_PARALLEL_CODE_SIGN="${COCOAPODS_PARALLEL_CODE_SIGN:-false}" 21 | SWIFT_STDLIB_PATH="${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" 22 | 23 | # Used as a return value for each invocation of `strip_invalid_archs` function. 24 | STRIP_BINARY_RETVAL=0 25 | 26 | # This protects against multiple targets copying the same framework dependency at the same time. The solution 27 | # was originally proposed here: https://lists.samba.org/archive/rsync/2008-February/020158.html 28 | RSYNC_PROTECT_TMP_FILES=(--filter "P .*.??????") 29 | 30 | # Copies and strips a vendored framework 31 | install_framework() 32 | { 33 | if [ -r "${BUILT_PRODUCTS_DIR}/$1" ]; then 34 | local source="${BUILT_PRODUCTS_DIR}/$1" 35 | elif [ -r "${BUILT_PRODUCTS_DIR}/$(basename "$1")" ]; then 36 | local source="${BUILT_PRODUCTS_DIR}/$(basename "$1")" 37 | elif [ -r "$1" ]; then 38 | local source="$1" 39 | fi 40 | 41 | local destination="${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 42 | 43 | if [ -L "${source}" ]; then 44 | echo "Symlinked..." 45 | source="$(readlink "${source}")" 46 | fi 47 | 48 | # Use filter instead of exclude so missing patterns don't throw errors. 49 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${destination}\"" 50 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${destination}" 51 | 52 | local basename 53 | basename="$(basename -s .framework "$1")" 54 | binary="${destination}/${basename}.framework/${basename}" 55 | 56 | if ! [ -r "$binary" ]; then 57 | binary="${destination}/${basename}" 58 | elif [ -L "${binary}" ]; then 59 | echo "Destination binary is symlinked..." 60 | dirname="$(dirname "${binary}")" 61 | binary="${dirname}/$(readlink "${binary}")" 62 | fi 63 | 64 | # Strip invalid architectures so "fat" simulator / device frameworks work on device 65 | if [[ "$(file "$binary")" == *"dynamically linked shared library"* ]]; then 66 | strip_invalid_archs "$binary" 67 | fi 68 | 69 | # Resign the code if required by the build settings to avoid unstable apps 70 | code_sign_if_enabled "${destination}/$(basename "$1")" 71 | 72 | # Embed linked Swift runtime libraries. No longer necessary as of Xcode 7. 73 | if [ "${XCODE_VERSION_MAJOR}" -lt 7 ]; then 74 | local swift_runtime_libs 75 | swift_runtime_libs=$(xcrun otool -LX "$binary" | grep --color=never @rpath/libswift | sed -E s/@rpath\\/\(.+dylib\).*/\\1/g | uniq -u) 76 | for lib in $swift_runtime_libs; do 77 | echo "rsync -auv \"${SWIFT_STDLIB_PATH}/${lib}\" \"${destination}\"" 78 | rsync -auv "${SWIFT_STDLIB_PATH}/${lib}" "${destination}" 79 | code_sign_if_enabled "${destination}/${lib}" 80 | done 81 | fi 82 | } 83 | 84 | # Copies and strips a vendored dSYM 85 | install_dsym() { 86 | local source="$1" 87 | if [ -r "$source" ]; then 88 | # Copy the dSYM into a the targets temp dir. 89 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${DERIVED_FILES_DIR}\"" 90 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${DERIVED_FILES_DIR}" 91 | 92 | local basename 93 | basename="$(basename -s .framework.dSYM "$source")" 94 | binary="${DERIVED_FILES_DIR}/${basename}.framework.dSYM/Contents/Resources/DWARF/${basename}" 95 | 96 | # Strip invalid architectures so "fat" simulator / device frameworks work on device 97 | if [[ "$(file "$binary")" == *"Mach-O "*"dSYM companion"* ]]; then 98 | strip_invalid_archs "$binary" 99 | fi 100 | 101 | if [[ $STRIP_BINARY_RETVAL == 1 ]]; then 102 | # Move the stripped file into its final destination. 103 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${DERIVED_FILES_DIR}/${basename}.framework.dSYM\" \"${DWARF_DSYM_FOLDER_PATH}\"" 104 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${DERIVED_FILES_DIR}/${basename}.framework.dSYM" "${DWARF_DSYM_FOLDER_PATH}" 105 | else 106 | # The dSYM was not stripped at all, in this case touch a fake folder so the input/output paths from Xcode do not reexecute this script because the file is missing. 107 | touch "${DWARF_DSYM_FOLDER_PATH}/${basename}.framework.dSYM" 108 | fi 109 | fi 110 | } 111 | 112 | # Copies the bcsymbolmap files of a vendored framework 113 | install_bcsymbolmap() { 114 | local bcsymbolmap_path="$1" 115 | local destination="${BUILT_PRODUCTS_DIR}" 116 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${bcsymbolmap_path}" "${destination}"" 117 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${bcsymbolmap_path}" "${destination}" 118 | } 119 | 120 | # Signs a framework with the provided identity 121 | code_sign_if_enabled() { 122 | if [ -n "${EXPANDED_CODE_SIGN_IDENTITY:-}" -a "${CODE_SIGNING_REQUIRED:-}" != "NO" -a "${CODE_SIGNING_ALLOWED}" != "NO" ]; then 123 | # Use the current code_sign_identity 124 | echo "Code Signing $1 with Identity ${EXPANDED_CODE_SIGN_IDENTITY_NAME}" 125 | local code_sign_cmd="/usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} ${OTHER_CODE_SIGN_FLAGS:-} --preserve-metadata=identifier,entitlements '$1'" 126 | 127 | if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then 128 | code_sign_cmd="$code_sign_cmd &" 129 | fi 130 | echo "$code_sign_cmd" 131 | eval "$code_sign_cmd" 132 | fi 133 | } 134 | 135 | # Strip invalid architectures 136 | strip_invalid_archs() { 137 | binary="$1" 138 | # Get architectures for current target binary 139 | binary_archs="$(lipo -info "$binary" | rev | cut -d ':' -f1 | awk '{$1=$1;print}' | rev)" 140 | # Intersect them with the architectures we are building for 141 | intersected_archs="$(echo ${ARCHS[@]} ${binary_archs[@]} | tr ' ' '\n' | sort | uniq -d)" 142 | # If there are no archs supported by this binary then warn the user 143 | if [[ -z "$intersected_archs" ]]; then 144 | echo "warning: [CP] Vendored binary '$binary' contains architectures ($binary_archs) none of which match the current build architectures ($ARCHS)." 145 | STRIP_BINARY_RETVAL=0 146 | return 147 | fi 148 | stripped="" 149 | for arch in $binary_archs; do 150 | if ! [[ "${ARCHS}" == *"$arch"* ]]; then 151 | # Strip non-valid architectures in-place 152 | lipo -remove "$arch" -output "$binary" "$binary" 153 | stripped="$stripped $arch" 154 | fi 155 | done 156 | if [[ "$stripped" ]]; then 157 | echo "Stripped $binary of architectures:$stripped" 158 | fi 159 | STRIP_BINARY_RETVAL=1 160 | } 161 | 162 | 163 | if [[ "$CONFIGURATION" == "Debug" ]]; then 164 | install_framework "${BUILT_PRODUCTS_DIR}/utopia/utopia.framework" 165 | fi 166 | if [[ "$CONFIGURATION" == "Release" ]]; then 167 | install_framework "${BUILT_PRODUCTS_DIR}/utopia/utopia.framework" 168 | fi 169 | if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then 170 | wait 171 | fi 172 | --------------------------------------------------------------------------------