├── Protocols ├── Bindable.swift ├── UIKit+Bindable.swift ├── Lifecycable.swift ├── NibLoadable.swift ├── Storyboardable.swift ├── CellViewModel.swift └── TableViewDataSource.swift ├── Playgrounds └── GenericTableViewController.playground.zip ├── Completions └── Completions.swift ├── README.md ├── Extensions ├── CoreGraphics+Extensions.swift ├── Extensions+Encodable.swift ├── StringHash.swift ├── UIColor+Hex.swift ├── Extensions+UITableView.swift ├── Extensions+Date.swift ├── UILabel.swift ├── Extensions+UIAlertController.swift ├── Extensions+Decodable.swift ├── String+Range.swift ├── UIView+Constraints.swift ├── UIImageExtension.swift ├── Extensions+NSLayoutConstraint.swift ├── Color.swift ├── Foundation+Extensions.swift ├── UIView+Stacking.swift ├── Autolayout.swift ├── UIKit+Extensions.swift └── Extensions+UIView.swift ├── Objective-C ├── NSWeakReference.h ├── NSWeakReference.m ├── RuntimeUtils.h └── RuntimeUtils.m ├── Other ├── Weak.swift ├── Observer.swift ├── SimpleSet.swift ├── ImageBuilder.swift ├── FileSize.swift ├── RuntimeUtils.swift ├── Reachability.swift ├── Result.swift ├── SeparatorView.swift ├── StringFormat.swift ├── Debugging.swift ├── MappedFile.swift ├── BinarySearch.swift ├── SimpleDictionary.swift ├── Font.swift ├── SwipeToDismissGestureRecognizer.swift ├── Unixtime.swift ├── DateFormat.swift └── DeviceMetrics.swift └── Generics ├── GenericCell.swift └── GenericController.swift /Protocols/Bindable.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public protocol Bindable { 4 | associatedtype Model 5 | func bind(to model: Model) 6 | } 7 | -------------------------------------------------------------------------------- /Playgrounds/GenericTableViewController.playground.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tesnikio/Useful-iOS/HEAD/Playgrounds/GenericTableViewController.playground.zip -------------------------------------------------------------------------------- /Protocols/UIKit+Bindable.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | public typealias BindableView = UIView & Bindable 4 | public typealias BindableTableViewCell = UITableViewCell & Bindable 5 | -------------------------------------------------------------------------------- /Completions/Completions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | typealias ItemClosure = (T) -> Void 4 | typealias OptionalItemClosure = (T?) -> Void 5 | typealias VoidClosure = () -> Void 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Useful iOS 2 | 3 | A list of awesome iOS categories, extensions, utilities and components. 4 | 5 | ![alt text](https://miro.medium.com/max/1920/1*nvrq6AYYeFDQr7YpZVMmMQ.png) 6 | -------------------------------------------------------------------------------- /Extensions/CoreGraphics+Extensions.swift: -------------------------------------------------------------------------------- 1 | import CoreGraphics 2 | 3 | extension CGSize { 4 | public init(dimension: CGFloat) { 5 | self.init(width: dimension, height: dimension) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Objective-C/NSWeakReference.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface NSWeakReference : NSObject 4 | 5 | @property (nonatomic, weak) instancetype value; 6 | 7 | - (instancetype)initWithValue:(instancetype)value; 8 | 9 | @end 10 | -------------------------------------------------------------------------------- /Other/Weak.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | final class Weak { 4 | private weak var _value: T? 5 | var value: T? { 6 | return self._value 7 | } 8 | 9 | init(_ value: T) { 10 | self._value = value 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Objective-C/NSWeakReference.m: -------------------------------------------------------------------------------- 1 | #import "NSWeakReference.h" 2 | 3 | @implementation NSWeakReference 4 | 5 | - (instancetype)initWithValue:(instancetype)value { 6 | self = [super init]; 7 | if (self != nil) { 8 | self.value = value; 9 | } 10 | return self; 11 | } 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /Other/Observer.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public class Observer { 4 | public weak var object: AnyObject? 5 | public var block: BlockValue? 6 | 7 | public init(object: AnyObject, 8 | block: BlockValue?) { 9 | self.object = object 10 | self.block = block 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Extensions/Extensions+Encodable.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Encodable { 4 | var dictionary: [String: Any]? { 5 | guard let data = try? JSONEncoder().encode(self) else { return nil } 6 | return (try? JSONSerialization.jsonObject(with: data, options: .allowFragments)).compactMap { $0 as? [String: Any] } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Extensions/StringHash.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension String { 4 | var persistentHashValue: UInt64 { 5 | var result = UInt64 (5381) 6 | let buf = [UInt8](self.utf8) 7 | for b in buf { 8 | result = 127 * (result & 0x00ffffffffffffff) + UInt64(b) 9 | } 10 | return result 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Extensions/UIColor+Hex.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UIColor { 4 | static func hex(_ hex: Int) -> UIColor { 5 | return UIColor( 6 | red: CGFloat(((hex >> 16) & 0xff)) / 255, 7 | green: CGFloat(((hex >> 8) & 0xff)) / 255, 8 | blue: CGFloat((hex & 0xff)) / 255, 9 | alpha: 1 10 | ) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Protocols/Lifecycable.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | @objc protocol Lifecycable { 4 | @objc optional func viewDidLoad() 5 | @objc optional func viewWillAppear() 6 | @objc optional func viewDidAppear() 7 | } 8 | 9 | //make them optional 10 | 11 | extension Lifecycable { 12 | func viewDidLoad() {} 13 | func viewWillAppear() {} 14 | func viewDidAppear() {} 15 | } 16 | 17 | // 18 | -------------------------------------------------------------------------------- /Extensions/Extensions+UITableView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UITableView { 4 | func register(nibModels: [CellType.Type]) { 5 | for model in nibModels { 6 | let identifier = String(describing: model.cellAnyType) 7 | let nib = UINib(nibName: identifier, bundle: nil) 8 | self.register(nib, forCellReuseIdentifier: identifier) 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Extensions/Extensions+Date.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Date { 4 | static func timeFromUnix(unixTime: Double) -> String { 5 | let timeInSeconds = TimeInterval(unixTime) 6 | let date = Date(timeIntervalSince1970: timeInSeconds) 7 | let dateFormatter = DateFormatter() 8 | dateFormatter.dateFormat = "HH:mm" 9 | return dateFormatter.string(from: date) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Extensions/UILabel.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UILabel { 4 | convenience init(text: String?, font: UIFont? = UIFont.systemFont(ofSize: 14), textColor: UIColor = .black, textAlignment: NSTextAlignment = .left, numberOfLines: Int = 1) { 5 | self.init() 6 | self.text = text 7 | self.font = font 8 | self.textColor = textColor 9 | self.textAlignment = textAlignment 10 | self.numberOfLines = numberOfLines 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Extensions/Extensions+UIAlertController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UIViewController { 4 | func showAlert(with title: String, and message: String) { 5 | let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) 6 | let okAction = UIAlertAction(title: "OK", style: .default, handler: nil) 7 | alertController.addAction(okAction) 8 | present(alertController, animated: true, completion: nil) 9 | } 10 | } 11 | 12 | -------------------------------------------------------------------------------- /Extensions/Extensions+Decodable.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Decodable { 4 | init(from: Any) throws { 5 | let data = try JSONSerialization.data(withJSONObject: from, options: .prettyPrinted) 6 | let decoder = JSONDecoder() 7 | let dateFormatter = DateFormatter() 8 | dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:sszzz" 9 | decoder.dateDecodingStrategy = .formatted(dateFormatter) 10 | self = try decoder.decode(Self.self, from: data) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Other/SimpleSet.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct SimpleSet { 4 | private var items: [T] = [] 5 | 6 | public init() {} 7 | 8 | public mutating func insert(_ item: T) { 9 | if !self.contains(item) { 10 | self.items.append(item) 11 | } 12 | } 13 | 14 | public func contains(_ item: T) -> Bool { 15 | for currentItem in self.items { 16 | if currentItem == item { 17 | return true 18 | } 19 | } 20 | return false 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Extensions/String+Range.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension String { 4 | public func range(from nsRange: NSRange) -> Range? { 5 | guard 6 | let from16 = utf16.index(utf16.startIndex, offsetBy: nsRange.location, limitedBy: utf16.endIndex), 7 | let to16 = utf16.index(utf16.startIndex, offsetBy: nsRange.location + nsRange.length, limitedBy: utf16.endIndex), 8 | let from = from16.samePosition(in: self), 9 | let to = to16.samePosition(in: self) 10 | else { return nil } 11 | return from ..< to 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Protocols/NibLoadable.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | protocol NibLoadable: class { 4 | static var nib: UINib { get } 5 | } 6 | 7 | extension NibLoadable { 8 | static var nib: UINib { 9 | return UINib(nibName: name, bundle: Bundle.init(for: self)) 10 | } 11 | 12 | static var name: String { 13 | return String(describing: self) 14 | } 15 | } 16 | 17 | extension NibLoadable where Self: UIView { 18 | static func loadFromNib() -> Self { 19 | guard let view = nib.instantiate(withOwner: nil, options: nil).first as? Self else { 20 | fatalError() 21 | } 22 | 23 | return view 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Other/ImageBuilder.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | public final class ImageBuilder { 4 | public class func image(withSize size: CGSize, byRoundingCorners: UIRectCorner = UIRectCorner.allCorners, 5 | cornerRadius: CGFloat = 0, color : UIColor) -> UIImage { 6 | UIGraphicsBeginImageContextWithOptions(size, false, 0); 7 | let path = UIBezierPath(roundedRect: CGRect(origin: CGPoint.zero, size: size), byRoundingCorners: byRoundingCorners, cornerRadii: CGSize(width:cornerRadius, height: cornerRadius)) 8 | color.setFill(); 9 | path.fill(); 10 | let res = UIGraphicsGetImageFromCurrentImageContext(); 11 | UIGraphicsEndImageContext() 12 | return res!; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Other/FileSize.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public func fileSize(_ path: String, useTotalFileAllocatedSize: Bool = false) -> Int? { 4 | if useTotalFileAllocatedSize { 5 | let url = URL(fileURLWithPath: path) 6 | if let values = (try? url.resourceValues(forKeys: Set([.isRegularFileKey, .totalFileAllocatedSizeKey]))) { 7 | if values.isRegularFile ?? false { 8 | if let fileSize = values.totalFileAllocatedSize { 9 | return fileSize 10 | } 11 | } 12 | } 13 | } 14 | 15 | var value = stat() 16 | if stat(path, &value) == 0 { 17 | return Int(value.st_size) 18 | } else { 19 | return nil 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Extensions/UIView+Constraints.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+Constraints.swift 3 | // EmbeddedControllers 4 | // 5 | // Created by Mikhail Rubanov on 23/12/2018. 6 | // Copyright © 2018 akaDuality. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIView { 12 | func pinToBounds(_ view: UIView) { 13 | view.translatesAutoresizingMaskIntoConstraints = false 14 | 15 | NSLayoutConstraint.activate([ 16 | view.topAnchor.constraint(equalTo: topAnchor), 17 | view.bottomAnchor.constraint(equalTo: bottomAnchor), 18 | view.leadingAnchor.constraint(equalTo: leadingAnchor), 19 | view.trailingAnchor.constraint(equalTo: trailingAnchor), 20 | ]) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Other/RuntimeUtils.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | private let systemVersion = { () -> (Int, Int) in 5 | let string = UIDevice.current.systemVersion as NSString 6 | var minor = 0 7 | let range = string.range(of: ".") 8 | if range.location != NSNotFound { 9 | minor = Int((string.substring(from: range.location + 1) as NSString).intValue) 10 | } 11 | return (Int(string.intValue), minor) 12 | }() 13 | 14 | public func matchMinimumSystemVersion(major: Int, minor: Int = 0) -> Bool { 15 | let version = systemVersion 16 | if version.0 == major { 17 | return version.1 >= minor 18 | } else if version.0 < major { 19 | return false 20 | } else { 21 | return true 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Protocols/Storyboardable.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | protocol Storyboardable { } 4 | 5 | extension Storyboardable where Self: UIViewController { 6 | static func instantiateInitialFromStoryboard() -> Self { 7 | let controller = storyboard().instantiateInitialViewController() 8 | return controller! as! Self 9 | } 10 | 11 | static func storyboard(fileName: String? = nil) -> UIStoryboard { 12 | let storyboard = UIStoryboard(name: fileName ?? storyboardIdentifier, bundle: nil) 13 | return storyboard 14 | } 15 | 16 | static var storyboardIdentifier: String { 17 | return String(describing: self) 18 | } 19 | 20 | static var storyboardName: String { 21 | return storyboardIdentifier 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Other/Reachability.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SystemConfiguration 3 | 4 | public class Reachability { 5 | public class var isConnectedToNetwork: Bool { 6 | var zeroAddress = sockaddr() 7 | zeroAddress.sa_len = UInt8(MemoryLayout.size) 8 | zeroAddress.sa_family = sa_family_t(AF_INET) 9 | 10 | guard let defaultRouteReachability = withUnsafePointer(to: &zeroAddress, { 11 | SCNetworkReachabilityCreateWithAddress(nil, UnsafePointer($0)) 12 | }) else { return false } 13 | 14 | var flags = SCNetworkReachabilityFlags() 15 | guard SCNetworkReachabilityGetFlags(defaultRouteReachability, &flags) else { return false } 16 | 17 | return flags.contains(.reachable) && !flags.contains(.connectionRequired) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Other/Result.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum Result { 4 | case success(T) 5 | case error(E) 6 | } 7 | 8 | public extension Result { 9 | var isSuccess: Bool { 10 | return !isError 11 | } 12 | 13 | var isError: Bool { 14 | switch self { 15 | case .error(_): 16 | return true 17 | case .success(_): 18 | return false 19 | } 20 | } 21 | 22 | var error: E? { 23 | switch self { 24 | case .success(_): 25 | return nil 26 | case .error(let err): 27 | return err 28 | } 29 | } 30 | 31 | var value: T? { 32 | switch self { 33 | case .success(let result): 34 | return result 35 | case .error(_): 36 | return nil 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Extensions/UIImageExtension.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UIImage { 4 | 5 | public func tintImageWith(tintColor: UIColor) -> UIImage { 6 | return self.tintedImageWithColor(tintColor: tintColor, blendMode: .destinationIn) 7 | } 8 | 9 | private func tintedImageWithColor(tintColor: UIColor, blendMode: CGBlendMode) -> UIImage { 10 | 11 | UIGraphicsBeginImageContextWithOptions(self.size, false, 0.0); 12 | 13 | tintColor.setFill() 14 | 15 | let bounds = CGRect.init(x: 0, y: 0, width: self.size.width, height: self.size.height) 16 | 17 | UIRectFill(bounds); 18 | 19 | self.draw(in: bounds, blendMode: blendMode, alpha: 1.0) 20 | 21 | if blendMode != .destinationIn { 22 | self.draw(in: bounds, blendMode: .destinationIn, alpha: 1.0) 23 | } 24 | 25 | let tintedImage = UIGraphicsGetImageFromCurrentImageContext(); 26 | UIGraphicsEndImageContext(); 27 | 28 | return tintedImage!; 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /Objective-C/RuntimeUtils.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | typedef enum { 4 | NSObjectAssociationPolicyRetain = 0, 5 | NSObjectAssociationPolicyCopy = 1 6 | } NSObjectAssociationPolicy; 7 | 8 | @interface RuntimeUtils : NSObject 9 | 10 | + (void)swizzleInstanceMethodOfClass:(Class)targetClass currentSelector:(SEL)currentSelector newSelector:(SEL)newSelector; 11 | + (void)swizzleInstanceMethodOfClass:(Class)targetClass currentSelector:(SEL)currentSelector withAnotherClass:(Class)anotherClass newSelector:(SEL)newSelector; 12 | + (void)swizzleClassMethodOfClass:(Class)targetClass currentSelector:(SEL)currentSelector newSelector:(SEL)newSelector; 13 | 14 | @end 15 | 16 | @interface NSObject (AssociatedObject) 17 | 18 | - (void)setAssociatedObject:(id)object forKey:(void const *)key; 19 | - (void)setAssociatedObject:(id)object forKey:(void const *)key associationPolicy:(NSObjectAssociationPolicy)associationPolicy; 20 | - (id)associatedObjectForKey:(void const *)key; 21 | - (bool)checkObjectIsKindOfClass:(Class)targetClass; 22 | - (void)setClass:(Class)newClass; 23 | 24 | @end 25 | -------------------------------------------------------------------------------- /Extensions/Extensions+NSLayoutConstraint.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension NSLayoutConstraint { 4 | static func quadroAspect(on view: UIView) -> NSLayoutConstraint { 5 | return NSLayoutConstraint.init(item: view, attribute: .height, relatedBy: .equal, toItem: view, attribute: .width, multiplier: 1, constant: 0) 6 | } 7 | 8 | static func contraints(withNewVisualFormat vf: String, dict: [String: Any]) -> [NSLayoutConstraint] { 9 | let separatedArray = vf.split(separator: ",") 10 | switch separatedArray.count { 11 | case 1: return NSLayoutConstraint.constraints(withVisualFormat: "\(separatedArray[0])", options: [], metrics: nil, views: dict) 12 | case 2: return NSLayoutConstraint.constraints(withVisualFormat: "\(separatedArray[0])", options: [], metrics: nil, views: dict) + NSLayoutConstraint.constraints(withVisualFormat: "\(separatedArray[1])", options: [], metrics: nil, views: dict) 13 | default: return NSLayoutConstraint.constraints(withVisualFormat: "\(separatedArray[0])", options: [], metrics: nil, views: dict) 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Extensions/Color.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | extension UIColor { 5 | 6 | static func transition(fromColor: UIColor, toColor: UIColor, progress: CGFloat) -> UIColor? { 7 | 8 | let toComponents = toColor.cgColor.components! 9 | 10 | guard let fromConvertedColor = fromColor.cgColor.converted(to: toColor.cgColor.colorSpace!, intent: .defaultIntent, options: nil) else { 11 | return nil 12 | } 13 | 14 | let fromComponents = fromConvertedColor.components! 15 | 16 | let componentsNumber = min(fromComponents.count, toComponents.count) 17 | 18 | var resultComponetns = [CGFloat]() 19 | 20 | for i in 0 ..< componentsNumber { 21 | let result = fromComponents[i] + (toComponents[i] - fromComponents[i]) * progress 22 | resultComponetns.append(result) 23 | } 24 | 25 | if let resultColor = CGColor.init(colorSpace: toColor.cgColor.colorSpace!, components: resultComponetns) { 26 | return UIColor.init(cgColor: resultColor) 27 | } 28 | 29 | return nil 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /Generics/GenericCell.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class GenericCell: UICollectionViewCell { 4 | var item: T! 5 | 6 | let separatorView: UIView = { 7 | let v = UIView() 8 | v.backgroundColor = UIColor(white: 0.6, alpha: 0.5) 9 | return v 10 | }() 11 | 12 | override init(frame: CGRect) { 13 | super.init(frame: frame) 14 | backgroundColor = .white 15 | setupViews() 16 | } 17 | 18 | func setupViews() {} 19 | 20 | required init?(coder aDecoder: NSCoder) { 21 | fatalError() 22 | } 23 | 24 | func addSeparatorView(leftPadding: CGFloat = 0) { 25 | addSubview(separatorView) 26 | separatorView.anchor(top: nil, leading: leadingAnchor, bottom: bottomAnchor, trailing: trailingAnchor, padding: .init(top: 0, left: leftPadding, bottom: 0, right: 0), size: .init(width: 0, height: 0.5)) 27 | } 28 | 29 | func addSeparatorView(leadingAnchor: NSLayoutXAxisAnchor) { 30 | addSubview(separatorView) 31 | separatorView.anchor(top: nil, leading: leadingAnchor, bottom: bottomAnchor, trailing: trailingAnchor, size: .init(width: 0, height: 0.5)) 32 | } 33 | } 34 | 35 | -------------------------------------------------------------------------------- /Extensions/Foundation+Extensions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension String { 4 | public var htmlToAttributedString: NSAttributedString? { 5 | guard let data = data(using: .utf8) else { return NSAttributedString() } 6 | do { 7 | return try NSAttributedString(data: data, options: [.documentType: NSAttributedString.DocumentType.html, .characterEncoding:String.Encoding.utf8.rawValue], documentAttributes: nil) 8 | } catch { 9 | return NSAttributedString() 10 | } 11 | } 12 | 13 | public var htmlToString: String { 14 | return htmlToAttributedString?.string ?? "" 15 | } 16 | 17 | public func uppercaseFirstLetterString() -> String { 18 | if self.isEmpty { 19 | return self 20 | } else { 21 | return String(self.prefix(1)).uppercased(with: Locale.current) + String(self.suffix(count - 1)) 22 | } 23 | } 24 | } 25 | 26 | extension Collection { 27 | 28 | /// Returns the element at the specified index if it is within bounds, otherwise nil. 29 | public subscript (safe index: Index) -> Element? { 30 | return indices.contains(index) ? self[index] : nil 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Other/SeparatorView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | internal final class SeparatorView: UIView { 4 | 5 | // MARK: Lifecycle 6 | internal init() { 7 | super.init(frame: .zero) 8 | translatesAutoresizingMaskIntoConstraints = false 9 | setUpConstraints() 10 | } 11 | 12 | internal required init?(coder aDecoder: NSCoder) { 13 | fatalError("init(coder:) has not been implemented") 14 | } 15 | 16 | // MARK: Internal 17 | internal override var intrinsicContentSize: CGSize { 18 | return CGSize(width: width, height: width) 19 | } 20 | 21 | internal var color: UIColor { 22 | get { return backgroundColor ?? .clear } 23 | set { backgroundColor = newValue } 24 | } 25 | 26 | internal var width: CGFloat = 1 { 27 | didSet { invalidateIntrinsicContentSize() } 28 | } 29 | 30 | // MARK: Private 31 | private func setUpConstraints() { 32 | setContentHuggingPriority(.defaultLow, for: .horizontal) 33 | setContentHuggingPriority(.defaultLow, for: .vertical) 34 | setContentCompressionResistancePriority(.defaultLow, for: .horizontal) 35 | setContentCompressionResistancePriority(.defaultLow, for: .vertical) 36 | } 37 | 38 | } 39 | 40 | -------------------------------------------------------------------------------- /Protocols/CellViewModel.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | protocol CellViewAnyModel { 4 | static var cellAnyType: UIView.Type { get } 5 | func setupAny(cell: UIView) 6 | } 7 | 8 | protocol CellViewModel: CellViewAnyModel { 9 | associatedtype CellType: UIView 10 | func setup(cell: CellType) 11 | } 12 | 13 | extension CellViewModel { 14 | static var cellAnyType: UIView.Type { 15 | return CellType.self 16 | } 17 | 18 | func setupAny(cell: UIView) { 19 | if let cell = cell as? CellType { 20 | setup(cell: cell) 21 | } else { 22 | assertionFailure("Wrong usage") 23 | } 24 | } 25 | } 26 | 27 | extension UITableView { 28 | func dequeueReusableCell(withModel model: CellViewAnyModel, for indexPath: IndexPath) -> UITableViewCell { 29 | let indetifier = String(describing: type(of: model).cellAnyType) 30 | let cell = self.dequeueReusableCell(withIdentifier: indetifier, for: indexPath) 31 | 32 | model.setupAny(cell: cell) 33 | return cell 34 | } 35 | 36 | func register(nibModels: [CellViewAnyModel.Type]) { 37 | for model in nibModels { 38 | let identifier = String(describing: model.cellAnyType) 39 | let nib = UINib(nibName: identifier, bundle: nil) 40 | self.register(nib, forCellReuseIdentifier: identifier) 41 | } 42 | } 43 | } 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /Other/StringFormat.swift: -------------------------------------------------------------------------------- 1 | public func dataSizeString(_ size: Int, forceDecimal: Bool = false, decimalSeparator: String = ".") -> String { 2 | return dataSizeString(Int64(size), forceDecimal: forceDecimal, decimalSeparator: decimalSeparator) 3 | } 4 | 5 | public func dataSizeString(_ size: Int64, forceDecimal: Bool = false, decimalSeparator: String = ".") -> String { 6 | if size >= 1024 * 1024 * 1024 { 7 | let remainder = Int64((Double(size % (1024 * 1024 * 1024)) / (1024 * 1024 * 102.4)).rounded(.down)) 8 | if remainder != 0 || forceDecimal { 9 | return "\(size / (1024 * 1024 * 1024))\(decimalSeparator)\(remainder) GB" 10 | } else { 11 | return "\(size / (1024 * 1024 * 1024)) GB" 12 | } 13 | } else if size >= 1024 * 1024 { 14 | let remainder = Int64((Double(size % (1024 * 1024)) / (1024.0 * 102.4)).rounded(.down)) 15 | if remainder != 0 || forceDecimal { 16 | return "\(size / (1024 * 1024))\(decimalSeparator)\(remainder) MB" 17 | } else { 18 | return "\(size / (1024 * 1024)) MB" 19 | } 20 | } else if size >= 1024 { 21 | let remainder = (size % (1024)) / (102) 22 | if remainder != 0 || forceDecimal { 23 | return "\(size / 1024)\(decimalSeparator)\(remainder) KB" 24 | } else { 25 | return "\(size / 1024) KB" 26 | } 27 | } else { 28 | return "\(size) B" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Other/Debugging.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public func currentTime() -> String { 4 | let timeString = DateFormatter.localizedString(from: Date(), dateStyle: .none, timeStyle: .long) 5 | return "[\(timeString)]" 6 | } 7 | 8 | public func log(_ message: String) { 9 | print("\(currentTime())\(message)") 10 | } 11 | 12 | /// The name/description of the current queue (Operation or Dispatch), if that can be found. Else, the name/description of the thread. 13 | public func queueName() -> String { 14 | if let currentOperationQueue = OperationQueue.current { 15 | if let currentDispatchQueue = currentOperationQueue.underlyingQueue { 16 | return "dispatch queue: \(currentDispatchQueue.label.nonEmpty ?? currentDispatchQueue.description)" 17 | } 18 | else { 19 | return "operation queue: \(currentOperationQueue.name?.nonEmpty ?? currentOperationQueue.description)" 20 | } 21 | } else { 22 | let currentThread = Thread.current 23 | return "UNKNOWN QUEUE on thread: \(currentThread.name?.nonEmpty ?? currentThread.description)" 24 | } 25 | } 26 | 27 | public func logWithQueueInfo(_ message: String) { 28 | log("\(queueName()) \(message)") 29 | } 30 | 31 | extension String { 32 | 33 | /// Returns this string if it is not empty, else `nil`. 34 | public var nonEmpty: String? { 35 | if self.isEmpty { 36 | return nil 37 | } 38 | else { 39 | return self 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Extensions/UIView+Stacking.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UIView { 4 | @discardableResult 5 | func withSize(size: CGSize) -> UIView { 6 | translatesAutoresizingMaskIntoConstraints = false 7 | widthAnchor.constraint(equalToConstant: size.width).isActive = true 8 | heightAnchor.constraint(equalToConstant: size.height).isActive = true 9 | return self 10 | } 11 | 12 | @discardableResult 13 | func withHeight(height: CGFloat) -> UIView { 14 | translatesAutoresizingMaskIntoConstraints = false 15 | heightAnchor.constraint(equalToConstant: height).isActive = true 16 | return self 17 | } 18 | 19 | @discardableResult 20 | func withWidth(_ width: CGFloat) -> UIView { 21 | translatesAutoresizingMaskIntoConstraints = false 22 | widthAnchor.constraint(equalToConstant: width).isActive = true 23 | return self 24 | } 25 | 26 | @discardableResult 27 | func withBorder(width: CGFloat, color: UIColor) -> UIView { 28 | layer.borderWidth = width 29 | layer.borderColor = color.cgColor 30 | return self 31 | } 32 | } 33 | 34 | extension UIEdgeInsets { 35 | static func allSides(side: CGFloat) -> UIEdgeInsets { 36 | return .init(top: side, left: side, bottom: side, right: side) 37 | } 38 | } 39 | 40 | extension UIImageView { 41 | convenience init(image: UIImage?, contentMode: UIView.ContentMode = .scaleAspectFill) { 42 | self.init(image: image) 43 | self.contentMode = contentMode 44 | self.clipsToBounds = true 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Other/MappedFile.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public final class MappedFile { 4 | private var handle: Int32 5 | private var currentSize: Int 6 | private var memory: UnsafeMutableRawPointer 7 | 8 | public init(path: String) { 9 | self.handle = open(path, O_RDWR | O_CREAT | O_APPEND, S_IRUSR | S_IWUSR) 10 | 11 | var value = stat() 12 | stat(path, &value) 13 | self.currentSize = Int(value.st_size) 14 | 15 | self.memory = mmap(nil, self.currentSize, PROT_READ | PROT_WRITE, MAP_SHARED, self.handle, 0) 16 | } 17 | 18 | deinit { 19 | munmap(self.memory, self.currentSize) 20 | close(self.handle) 21 | } 22 | 23 | public var size: Int { 24 | get { 25 | return self.currentSize 26 | } set(value) { 27 | if value != self.currentSize { 28 | munmap(self.memory, self.currentSize) 29 | ftruncate(self.handle, off_t(value)) 30 | self.currentSize = value 31 | self.memory = mmap(nil, self.currentSize, PROT_READ | PROT_WRITE, MAP_SHARED, self.handle, 0) 32 | } 33 | } 34 | } 35 | 36 | public func synchronize() { 37 | msync(self.memory, self.currentSize, MS_ASYNC) 38 | } 39 | 40 | public func write(at range: Range, from data: UnsafeRawPointer) { 41 | memcpy(self.memory.advanced(by: range.lowerBound), data, range.count) 42 | } 43 | 44 | public func read(at range: Range, to data: UnsafeMutableRawPointer) { 45 | memcpy(data, self.memory.advanced(by: range.lowerBound), range.count) 46 | } 47 | 48 | public func clear() { 49 | memset(self.memory, 0, self.currentSize) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Protocols/TableViewDataSource.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | public protocol TableViewCellConfigurator { 4 | static var reuseId: String { get } 5 | static var cellType: UITableViewCell.Type { get } 6 | func configure(cell: UITableViewCell) 7 | } 8 | 9 | public struct CellConfigurator: TableViewCellConfigurator where TCell.Model == TModel { 10 | public static var cellType: UITableViewCell.Type { return TCell.self } 11 | public static var reuseId: String { return String(describing: cellType) } 12 | 13 | public let model: TModel 14 | 15 | public init(model: TModel) { 16 | self.model = model 17 | } 18 | 19 | public func configure(cell: UITableViewCell) { 20 | guard let bindableCell = cell as? TCell else { assert(false); return } 21 | bindableCell.bind(to: model) 22 | } 23 | } 24 | 25 | public protocol TableViewSectionConfigurator { 26 | var cellConfigurators: [TableViewCellConfigurator] { get } 27 | } 28 | 29 | public struct SectionConfigurator: TableViewSectionConfigurator { 30 | public let cellConfigurators: [TableViewCellConfigurator] 31 | 32 | public init(cellConfigurators: [TableViewCellConfigurator]) { 33 | self.cellConfigurators = cellConfigurators 34 | } 35 | } 36 | 37 | public class TableViewDataSource: NSObject, UITableViewDataSource { 38 | 39 | public var sectionConfigurations: [TableViewSectionConfigurator] = [] 40 | 41 | public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 42 | guard let cellConfigurator = sectionConfigurations[safe: indexPath.section]?.cellConfigurators[safe: indexPath.row] else { 43 | return UITableViewCell() 44 | } 45 | let cellConfiguratorType = type(of: cellConfigurator) 46 | tableView.register(cellConfiguratorType.cellType, forCellReuseIdentifier: cellConfiguratorType.reuseId) 47 | let cell = tableView.dequeueReusableCell(withIdentifier: cellConfiguratorType.reuseId, for: indexPath) 48 | cellConfigurator.configure(cell: cell) 49 | return cell 50 | } 51 | 52 | public func numberOfSections(in tableView: UITableView) -> Int { 53 | return sectionConfigurations.count 54 | } 55 | 56 | public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 57 | return sectionConfigurations[section].cellConfigurators.count 58 | } 59 | } 60 | 61 | 62 | -------------------------------------------------------------------------------- /Generics/GenericController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class GenericController, U, H: UICollectionReusableView>: UICollectionViewController { 4 | 5 | var items = [U]() 6 | 7 | fileprivate let cellId = "cellId" 8 | fileprivate let headerId = "headerId" 9 | 10 | override func viewDidLoad() { 11 | super.viewDidLoad() 12 | collectionView.backgroundColor = .white 13 | 14 | collectionView.register(T.self, forCellWithReuseIdentifier: cellId) 15 | collectionView.register(H.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: headerId) 16 | } 17 | 18 | fileprivate func setupRefreshControl() { 19 | let rc = UIRefreshControl() 20 | rc.addTarget(self, action: #selector(handleRefresh), for: .valueChanged) 21 | collectionView.refreshControl = rc 22 | } 23 | 24 | @objc func handleRefresh() { 25 | collectionView.refreshControl?.endRefreshing() 26 | } 27 | 28 | override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 29 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! T 30 | cell.item = items[indexPath.row] 31 | return cell 32 | } 33 | 34 | override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { 35 | let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: headerId, for: indexPath) 36 | return header 37 | } 38 | 39 | override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 40 | return items.count 41 | } 42 | 43 | override func collectionView(_ collectionView: UICollectionView, willDisplaySupplementaryView view: UICollectionReusableView, forElementKind elementKind: String, at indexPath: IndexPath) { 44 | view.layer.zPosition = -1 45 | } 46 | 47 | init(scrollDirection: UICollectionView.ScrollDirection = .vertical) { 48 | let layout = UICollectionViewFlowLayout() 49 | layout.scrollDirection = scrollDirection 50 | super.init(collectionViewLayout: layout) 51 | } 52 | 53 | required init?(coder aDecoder: NSCoder) { 54 | fatalError() 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /Other/BinarySearch.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | func binarySearch(_ inputArr: [A], extract: (A) -> T, searchItem: T) -> Int? { 4 | var lowerIndex = 0 5 | var upperIndex = inputArr.count - 1 6 | 7 | if lowerIndex > upperIndex { 8 | return nil 9 | } 10 | 11 | while true { 12 | let currentIndex = (lowerIndex + upperIndex) / 2 13 | let value = extract(inputArr[currentIndex]) 14 | 15 | if value == searchItem { 16 | return currentIndex 17 | } else if lowerIndex > upperIndex { 18 | return nil 19 | } else { 20 | if (value > searchItem) { 21 | upperIndex = currentIndex - 1 22 | } else { 23 | lowerIndex = currentIndex + 1 24 | } 25 | } 26 | } 27 | } 28 | 29 | func binarySearch(_ inputArr: [T], searchItem: T) -> Int? { 30 | var lowerIndex = 0; 31 | var upperIndex = inputArr.count - 1 32 | 33 | if lowerIndex > upperIndex { 34 | return nil 35 | } 36 | 37 | while (true) { 38 | let currentIndex = (lowerIndex + upperIndex) / 2 39 | if (inputArr[currentIndex] == searchItem) { 40 | return currentIndex 41 | } else if (lowerIndex > upperIndex) { 42 | return nil 43 | } else { 44 | if (inputArr[currentIndex] > searchItem) { 45 | upperIndex = currentIndex - 1 46 | } else { 47 | lowerIndex = currentIndex + 1 48 | } 49 | } 50 | } 51 | } 52 | 53 | func binaryInsertionIndex(_ inputArr: [A], extract: (A) -> T, searchItem: T) -> Int { 54 | var lo = 0 55 | var hi = inputArr.count - 1 56 | while lo <= hi { 57 | let mid = (lo + hi) / 2 58 | let value = extract(inputArr[mid]) 59 | if value < searchItem { 60 | lo = mid + 1 61 | } else if searchItem < value { 62 | hi = mid - 1 63 | } else { 64 | return mid 65 | } 66 | } 67 | return lo 68 | } 69 | 70 | func binaryInsertionIndex(_ inputArr: [T], searchItem: T) -> Int { 71 | var lo = 0 72 | var hi = inputArr.count - 1 73 | while lo <= hi { 74 | let mid = (lo + hi) / 2 75 | if inputArr[mid] < searchItem { 76 | lo = mid + 1 77 | } else if searchItem < inputArr[mid] { 78 | hi = mid - 1 79 | } else { 80 | return mid 81 | } 82 | } 83 | return lo 84 | } 85 | 86 | func binaryInsertionIndexReverse(_ inputArr: [T], searchItem: T) -> Int { 87 | var lo = 0 88 | var hi = inputArr.count - 1 89 | while lo <= hi { 90 | let mid = (lo + hi) / 2 91 | if inputArr[mid] > searchItem { 92 | lo = mid + 1 93 | } else if searchItem > inputArr[mid] { 94 | hi = mid - 1 95 | } else { 96 | return mid 97 | } 98 | } 99 | return lo 100 | } 101 | -------------------------------------------------------------------------------- /Other/SimpleDictionary.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct SimpleDictionary: Sequence { 4 | private var items: [(K, V)] = [] 5 | 6 | public var count: Int { 7 | return self.items.count 8 | } 9 | 10 | public var isEmpty: Bool { 11 | return self.items.isEmpty 12 | } 13 | 14 | public init() {} 15 | 16 | public init(_ dict: [K: V]) { 17 | for (k, v) in dict { 18 | self.items.append((k, v)) 19 | } 20 | } 21 | 22 | private init(items: [(K, V)] = []) { 23 | self.items = items 24 | } 25 | 26 | public func filteredOut(keysIn: Set) -> SimpleDictionary? { 27 | var hasUpdates = false 28 | for (key, _) in self.items { 29 | if keysIn.contains(key) { 30 | hasUpdates = true 31 | break 32 | } 33 | } 34 | if hasUpdates { 35 | var updatedItems: [(K, V)] = [] 36 | for (key, value) in self.items { 37 | if !keysIn.contains(key) { 38 | updatedItems.append((key, value)) 39 | break 40 | } 41 | } 42 | return SimpleDictionary(items: updatedItems) 43 | } else { 44 | return nil 45 | } 46 | } 47 | 48 | public subscript(key: K) -> V? { 49 | get { 50 | for (k, value) in self.items { 51 | if k == key { 52 | return value 53 | } 54 | } 55 | return nil 56 | } set(value) { 57 | var index = 0 58 | for (k, _) in self.items { 59 | if k == key { 60 | if let value = value { 61 | self.items[index] = (k, value) 62 | } else { 63 | self.items.remove(at: index) 64 | } 65 | return 66 | } 67 | index += 1 68 | } 69 | if let value = value { 70 | self.items.append((key, value)) 71 | } 72 | } 73 | } 74 | 75 | public func makeIterator() -> AnyIterator<(K, V)> { 76 | var index = 0 77 | return AnyIterator { () -> (K, V)? in 78 | if index < self.items.count { 79 | let currentIndex = index 80 | index += 1 81 | return self.items[currentIndex] 82 | } 83 | return nil 84 | } 85 | } 86 | 87 | public func isEqual(other: SimpleDictionary, with f: (V, V) -> Bool) -> Bool { 88 | if self.items.count != other.items.count { 89 | return false 90 | } 91 | for i in 0 ..< self.items.count { 92 | if self.items[i].0 != other.items[i].0 { 93 | return false 94 | } 95 | if !f(self.items[i].1, other.items[i].1) { 96 | return false 97 | } 98 | } 99 | 100 | return true 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /Other/Font.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | public struct Font { 5 | public static func regular(_ size: CGFloat) -> UIFont { 6 | return UIFont.systemFont(ofSize: size) 7 | } 8 | 9 | public static func medium(_ size: CGFloat) -> UIFont { 10 | if #available(iOS 8.2, *) { 11 | return UIFont.systemFont(ofSize: size, weight: UIFont.Weight.medium) 12 | } else { 13 | return CTFontCreateWithName("HelveticaNeue-Medium" as CFString, size, nil) 14 | } 15 | } 16 | 17 | public static func semibold(_ size: CGFloat) -> UIFont { 18 | if #available(iOS 8.2, *) { 19 | return UIFont.systemFont(ofSize: size, weight: UIFont.Weight.semibold) 20 | } else { 21 | return CTFontCreateWithName("HelveticaNeue-Medium" as CFString, size, nil) 22 | } 23 | } 24 | 25 | public static func bold(_ size: CGFloat) -> UIFont { 26 | if #available(iOS 8.2, *) { 27 | return UIFont.boldSystemFont(ofSize: size) 28 | } else { 29 | return CTFontCreateWithName("HelveticaNeue-Bold" as CFString, size, nil) 30 | } 31 | } 32 | 33 | public static func light(_ size: CGFloat) -> UIFont { 34 | if #available(iOS 8.2, *) { 35 | return UIFont.systemFont(ofSize: size, weight: UIFont.Weight.light) 36 | } else { 37 | return CTFontCreateWithName("HelveticaNeue-Light" as CFString, size, nil) 38 | } 39 | } 40 | 41 | public static func semiboldItalic(_ size: CGFloat) -> UIFont { 42 | if let descriptor = UIFont.systemFont(ofSize: size).fontDescriptor.withSymbolicTraits([.traitBold, .traitItalic]) { 43 | return UIFont(descriptor: descriptor, size: size) 44 | } else { 45 | return UIFont.italicSystemFont(ofSize: size) 46 | } 47 | } 48 | 49 | public static func monospace(_ size: CGFloat) -> UIFont { 50 | return UIFont(name: "Menlo-Regular", size: size - 1.0) ?? UIFont.systemFont(ofSize: size) 51 | } 52 | 53 | public static func semiboldMonospace(_ size: CGFloat) -> UIFont { 54 | return UIFont(name: "Menlo-Bold", size: size - 1.0) ?? UIFont.systemFont(ofSize: size) 55 | } 56 | 57 | public static func italicMonospace(_ size: CGFloat) -> UIFont { 58 | return UIFont(name: "Menlo-Italic", size: size - 1.0) ?? UIFont.systemFont(ofSize: size) 59 | } 60 | 61 | public static func semiboldItalicMonospace(_ size: CGFloat) -> UIFont { 62 | return UIFont(name: "Menlo-BoldItalic", size: size - 1.0) ?? UIFont.systemFont(ofSize: size) 63 | } 64 | 65 | public static func italic(_ size: CGFloat) -> UIFont { 66 | return UIFont.italicSystemFont(ofSize: size) 67 | } 68 | } 69 | 70 | public extension NSAttributedString { 71 | convenience init(string: String, font: UIFont? = nil, textColor: UIColor = UIColor.black, paragraphAlignment: NSTextAlignment? = nil) { 72 | var attributes: [NSAttributedStringKey: AnyObject] = [:] 73 | if let font = font { 74 | attributes[NSAttributedStringKey.font] = font 75 | } 76 | attributes[NSAttributedStringKey.foregroundColor] = textColor 77 | if let paragraphAlignment = paragraphAlignment { 78 | let paragraphStyle = NSMutableParagraphStyle() 79 | paragraphStyle.alignment = paragraphAlignment 80 | attributes[NSAttributedStringKey.paragraphStyle] = paragraphStyle 81 | } 82 | self.init(string: string, attributes: attributes) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Other/SwipeToDismissGestureRecognizer.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit.UIGestureRecognizerSubclass 3 | 4 | private func traceScrollView(view: UIView, point: CGPoint) -> UIScrollView? { 5 | for subview in view.subviews { 6 | let subviewPoint = view.convert(point, to: subview) 7 | if subview.frame.contains(point), let result = traceScrollView(view: subview, point: subviewPoint) { 8 | return result 9 | } 10 | } 11 | if let scrollView = view as? UIScrollView { 12 | return scrollView 13 | } 14 | return nil 15 | } 16 | 17 | class SwipeToDismissGestureRecognizer: UIGestureRecognizer, UIGestureRecognizerDelegate { 18 | private var beginPosition = CGPoint() 19 | 20 | override init(target: Any?, action: Selector?) { 21 | super.init(target: target, action: action) 22 | 23 | self.delegate = self 24 | } 25 | 26 | override func reset() { 27 | super.reset() 28 | 29 | self.state = .possible 30 | } 31 | 32 | func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { 33 | return true 34 | } 35 | 36 | override func touchesBegan(_ touches: Set, with event: UIEvent) { 37 | super.touchesBegan(touches, with: event) 38 | 39 | guard let touch = touches.first, let view = self.view else { 40 | self.state = .failed 41 | return 42 | } 43 | 44 | var found = false 45 | let point = touch.location(in: self.view) 46 | if let scrollView = traceScrollView(view: view, point: point) { 47 | let contentOffset = scrollView.contentOffset 48 | let contentInset = scrollView.contentInset 49 | if contentOffset.y.isLessThanOrEqualTo(contentInset.top) { 50 | found = true 51 | } 52 | } 53 | if found { 54 | self.beginPosition = point 55 | } else { 56 | self.state = .failed 57 | } 58 | } 59 | 60 | override func touchesMoved(_ touches: Set, with event: UIEvent) { 61 | super.touchesMoved(touches, with: event) 62 | 63 | guard let touch = touches.first, let view = self.view else { 64 | self.state = .failed 65 | return 66 | } 67 | 68 | let point = touch.location(in: self.view) 69 | 70 | let translation = point.offsetBy(dx: -self.beginPosition.x, dy: -self.beginPosition.y) 71 | 72 | if self.state == .possible { 73 | if abs(translation.x) > 5.0 { 74 | self.state = .failed 75 | return 76 | } 77 | var lockDown = false 78 | let point = touch.location(in: self.view) 79 | if let scrollView = traceScrollView(view: view, point: point) { 80 | let contentOffset = scrollView.contentOffset 81 | let contentInset = scrollView.contentInset 82 | if contentOffset.y.isLessThanOrEqualTo(contentInset.top) { 83 | lockDown = true 84 | } 85 | } 86 | if lockDown { 87 | if translation.y > 2.0 { 88 | self.state = .began 89 | } 90 | } else { 91 | self.state = .failed 92 | } 93 | } else { 94 | self.state = .changed 95 | } 96 | } 97 | 98 | override func touchesEnded(_ touches: Set, with event: UIEvent) { 99 | super.touchesEnded(touches, with: event) 100 | 101 | self.state = .failed 102 | } 103 | 104 | func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool { 105 | if otherGestureRecognizer is UIPanGestureRecognizer { 106 | return true 107 | } 108 | return false 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /Extensions/Autolayout.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | public enum Centering { 4 | case horizontally 5 | case vertically 6 | } 7 | 8 | extension UIView { 9 | 10 | public func pinTo(layoutGuide: UILayoutGuide, withEdges edges: UIRectEdge) -> [NSLayoutConstraint] { 11 | var result = [NSLayoutConstraint]() 12 | if edges.contains(.left) { 13 | result.append(layoutGuide.leftAnchor.constraint(equalTo: self.leftAnchor)) 14 | } 15 | if edges.contains(.top) { 16 | result.append(layoutGuide.topAnchor.constraint(equalTo: self.topAnchor)) 17 | } 18 | if edges.contains(.right) { 19 | result.append(layoutGuide.rightAnchor.constraint(equalTo: self.rightAnchor)) 20 | } 21 | if edges.contains(.bottom) { 22 | result.append(layoutGuide.bottomAnchor.constraint(equalTo: self.bottomAnchor)) 23 | } 24 | return result 25 | } 26 | 27 | public func pinTo(view: UIView, withEdges edges: UIRectEdge) -> [NSLayoutConstraint] { 28 | var result = [NSLayoutConstraint]() 29 | if edges.contains(.left) { 30 | result.append(view.leftAnchor.constraint(equalTo: self.leftAnchor)) 31 | } 32 | if edges.contains(.top) { 33 | result.append(view.topAnchor.constraint(equalTo: self.topAnchor)) 34 | } 35 | if edges.contains(.right) { 36 | result.append(view.rightAnchor.constraint(equalTo: self.rightAnchor)) 37 | } 38 | if edges.contains(.bottom) { 39 | result.append(view.bottomAnchor.constraint(equalTo: self.bottomAnchor)) 40 | } 41 | return result 42 | } 43 | 44 | public func pinToParent() -> [NSLayoutConstraint] { 45 | guard let parent = superview else { assert(false); return [] } 46 | return self.pinTo(view: parent, withEdges: .all) 47 | } 48 | 49 | public func pinTo(view: UIView, withInsets insets: UIEdgeInsets) -> [NSLayoutConstraint] { 50 | var result = [NSLayoutConstraint]() 51 | result.append(self.leftAnchor.constraint(equalTo: view.leftAnchor, constant: insets.left)) 52 | result.append(self.topAnchor.constraint(equalTo: view.topAnchor, constant: insets.top)) 53 | result.append(view.rightAnchor.constraint(equalTo: self.rightAnchor, constant: insets.right)) 54 | result.append(view.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: insets.bottom)) 55 | return result 56 | } 57 | 58 | public func pinToParent(withInsets insets: UIEdgeInsets) -> [NSLayoutConstraint] { 59 | guard let parent = superview else { assert(false); return [] } 60 | return self.pinTo(view: parent, withInsets: insets) 61 | } 62 | 63 | public func pinToParentSafe() -> [NSLayoutConstraint] { 64 | guard let parent = superview else { assert(false); return [] } 65 | let layoutGuide = parent.safeAreaLayoutGuide 66 | return self.pinTo(layoutGuide: layoutGuide, withEdges: .all) 67 | } 68 | 69 | public func pinToParent(withEdges edges: UIRectEdge) -> [NSLayoutConstraint] { 70 | guard let parent = superview else { assert(false); return [] } 71 | return self.pinTo(view: parent, withEdges: edges) 72 | } 73 | 74 | public func pinToParentSafe(withEdges edges: UIRectEdge) -> [NSLayoutConstraint] { 75 | guard let parent = superview else { assert(false); return [] } 76 | let layoutGuide = parent.safeAreaLayoutGuide 77 | return self.pinTo(layoutGuide: layoutGuide, withEdges: edges) 78 | } 79 | 80 | public func centerIn(view: UIView, _ centering: Centering) -> [NSLayoutConstraint] { 81 | var result = [NSLayoutConstraint]() 82 | switch centering { 83 | case .horizontally: 84 | result.append(view.centerXAnchor.constraint(equalTo: self.centerXAnchor)) 85 | case .vertically: 86 | result.append(view.centerYAnchor.constraint(equalTo: self.centerYAnchor)) 87 | } 88 | return result 89 | } 90 | 91 | public func centerInParent(_ centering: Centering) -> [NSLayoutConstraint] { 92 | guard let parent = superview else { assert(false); return [] } 93 | return centerIn(view: parent, centering) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Objective-C/RuntimeUtils.m: -------------------------------------------------------------------------------- 1 | #import "RuntimeUtils.h" 2 | 3 | #import 4 | 5 | @implementation RuntimeUtils 6 | 7 | + (void)swizzleInstanceMethodOfClass:(Class)targetClass currentSelector:(SEL)currentSelector newSelector:(SEL)newSelector { 8 | Method origMethod = nil, newMethod = nil; 9 | 10 | origMethod = class_getInstanceMethod(targetClass, currentSelector); 11 | newMethod = class_getInstanceMethod(targetClass, newSelector); 12 | if ((origMethod != nil) && (newMethod != nil)) { 13 | if(class_addMethod(targetClass, currentSelector, method_getImplementation(newMethod), method_getTypeEncoding(newMethod))) { 14 | class_replaceMethod(targetClass, newSelector, method_getImplementation(origMethod), method_getTypeEncoding(origMethod)); 15 | } else { 16 | method_exchangeImplementations(origMethod, newMethod); 17 | } 18 | } 19 | } 20 | 21 | + (void)swizzleInstanceMethodOfClass:(Class)targetClass currentSelector:(SEL)currentSelector withAnotherClass:(Class)anotherClass newSelector:(SEL)newSelector { 22 | Method origMethod = nil, newMethod = nil; 23 | 24 | origMethod = class_getInstanceMethod(targetClass, currentSelector); 25 | newMethod = class_getInstanceMethod(anotherClass, newSelector); 26 | if ((origMethod != nil) && (newMethod != nil)) { 27 | method_exchangeImplementations(origMethod, newMethod); 28 | } 29 | } 30 | 31 | + (void)swizzleClassMethodOfClass:(Class)targetClass currentSelector:(SEL)currentSelector newSelector:(SEL)newSelector { 32 | Method origMethod = nil, newMethod = nil; 33 | 34 | origMethod = class_getClassMethod(targetClass, currentSelector); 35 | newMethod = class_getClassMethod(targetClass, newSelector); 36 | 37 | targetClass = object_getClass((instancetype)targetClass); 38 | 39 | if ((origMethod != nil) && (newMethod != nil)) { 40 | if(class_addMethod(targetClass, currentSelector, method_getImplementation(newMethod), method_getTypeEncoding(newMethod))) { 41 | class_replaceMethod(targetClass, newSelector, method_getImplementation(origMethod), method_getTypeEncoding(origMethod)); 42 | } else { 43 | method_exchangeImplementations(origMethod, newMethod); 44 | } 45 | } 46 | } 47 | 48 | @end 49 | 50 | @implementation NSObject (AssociatedObject) 51 | 52 | - (void)setAssociatedObject:(instancetype)object forKey:(void const *)key 53 | { 54 | [self setAssociatedObject:object forKey:key associationPolicy:NSObjectAssociationPolicyRetain]; 55 | } 56 | 57 | - (void)setAssociatedObject:(instancetype)object forKey:(void const *)key associationPolicy:(NSObjectAssociationPolicy)associationPolicy { 58 | int policy = 0; 59 | switch (associationPolicy) { 60 | case NSObjectAssociationPolicyRetain: 61 | policy = OBJC_ASSOCIATION_RETAIN_NONATOMIC; 62 | break; 63 | case NSObjectAssociationPolicyCopy: 64 | policy = OBJC_ASSOCIATION_COPY_NONATOMIC; 65 | break; 66 | default: 67 | policy = OBJC_ASSOCIATION_RETAIN_NONATOMIC; 68 | break; 69 | } 70 | objc_setAssociatedObject(self, key, object, policy); 71 | } 72 | 73 | - (instancetype)associatedObjectForKey:(void const *)key { 74 | return objc_getAssociatedObject(self, key); 75 | } 76 | 77 | - (bool)checkObjectIsKindOfClass:(Class)targetClass { 78 | return [self isKindOfClass:targetClass]; 79 | } 80 | 81 | - (void)setClass:(Class)newClass { 82 | object_setClass(self, newClass); 83 | } 84 | 85 | static Class freedomMakeClass(Class superclass, Class subclass, SEL *copySelectors, int copySelectorsCount) { 86 | if (superclass == Nil || subclass == Nil) 87 | return nil; 88 | 89 | Class decoratedClass = objc_allocateClassPair(superclass, [[NSString alloc] initWithFormat:@"%@_%@", NSStringFromClass(superclass), NSStringFromClass(subclass)].UTF8String, 0); 90 | 91 | unsigned int count = 0; 92 | Method *methodList = class_copyMethodList(subclass, &count); 93 | if (methodList != NULL) { 94 | for (unsigned int i = 0; i < count; i++) { 95 | SEL methodName = method_getName(methodList[i]); 96 | class_addMethod(decoratedClass, methodName, method_getImplementation(methodList[i]), method_getTypeEncoding(methodList[i])); 97 | } 98 | 99 | free(methodList); 100 | } 101 | 102 | objc_registerClassPair(decoratedClass); 103 | 104 | return decoratedClass; 105 | } 106 | 107 | @end 108 | -------------------------------------------------------------------------------- /Other/Unixtime.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct DateTime { 4 | public let seconds: Int32 // 0 ... 59 5 | public let minutes: Int32 // 0 ... 59 6 | public let hours: Int32 // 0 ... 23 7 | public let dayOfMonth: Int32 // 1 ... 31 8 | public let month: Int32 // 0 ... 11 9 | public let year: Int32 // since 1900 10 | public let dayOfWeek: Int32 // 0 ... 6 11 | public let dayOfYear: Int32 // 0 ... 365 12 | } 13 | 14 | private let daysSinceJan1st: [[UInt32]] = [ 15 | [0,31,59,90,120,151,181,212,243,273,304,334,365], // 365 days, non-leap 16 | [0,31,60,91,121,152,182,213,244,274,305,335,366] // 366 days, leap 17 | ] 18 | 19 | public func secondsSinceEpochToDateTime(_ secondsSinceEpoch: Int64) -> DateTime { 20 | var sec: UInt64 21 | let quadricentennials: UInt32 22 | var centennials: UInt32 23 | var quadrennials: UInt32 24 | var annuals: UInt32 25 | let year: UInt32 26 | let leap: UInt32 27 | let yday: UInt32 28 | let hour: UInt32 29 | let min: UInt32 30 | var month: UInt32 31 | var mday: UInt32 32 | let wday: UInt32 33 | 34 | /* 35 | 400 years: 36 | 37 | 1st hundred, starting immediately after a leap year that's a multiple of 400: 38 | n n n l \ 39 | n n n l } 24 times 40 | ... / 41 | n n n l / 42 | n n n n 43 | 44 | 2nd hundred: 45 | n n n l \ 46 | n n n l } 24 times 47 | ... / 48 | n n n l / 49 | n n n n 50 | 51 | 3rd hundred: 52 | n n n l \ 53 | n n n l } 24 times 54 | ... / 55 | n n n l / 56 | n n n n 57 | 58 | 4th hundred: 59 | n n n l \ 60 | n n n l } 24 times 61 | ... / 62 | n n n l / 63 | n n n L <- 97'th leap year every 400 years 64 | */ 65 | 66 | // Re-bias from 1970 to 1601: 67 | // 1970 - 1601 = 369 = 3*100 + 17*4 + 1 years (incl. 89 leap days) = 68 | // (3*100*(365+24/100) + 17*4*(365+1/4) + 1*365)*24*3600 seconds 69 | sec = UInt64(secondsSinceEpoch) + (11644473600 as UInt64) 70 | 71 | wday = (uint)((sec / 86400 + 1) % 7); // day of week 72 | 73 | // Remove multiples of 400 years (incl. 97 leap days) 74 | quadricentennials = UInt32((UInt64(sec) / (12622780800 as UInt64))) // 400*365.2425*24*3600 75 | sec %= 12622780800 as UInt64 76 | 77 | // Remove multiples of 100 years (incl. 24 leap days), can't be more than 3 78 | // (because multiples of 4*100=400 years (incl. leap days) have been removed) 79 | centennials = UInt32(UInt64(sec) / (3155673600 as UInt64)) // 100*(365+24/100)*24*3600 80 | if centennials > 3 { 81 | centennials = 3 82 | } 83 | sec -= UInt64(centennials) * (3155673600 as UInt64) 84 | 85 | // Remove multiples of 4 years (incl. 1 leap day), can't be more than 24 86 | // (because multiples of 25*4=100 years (incl. leap days) have been removed) 87 | quadrennials = UInt32((UInt64(sec) / (126230400 as UInt64))) // 4*(365+1/4)*24*3600 88 | if quadrennials > 24 { 89 | quadrennials = 24 90 | } 91 | sec -= UInt64(quadrennials) * (126230400 as UInt64) 92 | 93 | // Remove multiples of years (incl. 0 leap days), can't be more than 3 94 | // (because multiples of 4 years (incl. leap days) have been removed) 95 | annuals = UInt32(sec / (31536000 as UInt64)) // 365*24*3600 96 | if annuals > 3 { 97 | annuals = 3 98 | } 99 | sec -= UInt64(annuals) * (31536000 as UInt64) 100 | 101 | // Calculate the year and find out if it's leap 102 | year = 1601 + quadricentennials * 400 + centennials * 100 + quadrennials * 4 + annuals; 103 | leap = (!(year % UInt32(4) != 0) && ((year % UInt32(100) != 0) || !(year % UInt32(400) != 0))) ? 1 : 0 104 | 105 | // Calculate the day of the year and the time 106 | yday = UInt32(sec / (86400 as UInt64)) 107 | sec %= 86400; 108 | hour = UInt32(sec / 3600); 109 | sec %= 3600; 110 | min = UInt32(sec / 60); 111 | sec %= 60; 112 | 113 | mday = 1 114 | month = 1 115 | while month < 13 { 116 | if (yday < daysSinceJan1st[Int(leap)][Int(month)]) { 117 | mday += yday - daysSinceJan1st[Int(leap)][Int(month - 1)] 118 | break 119 | } 120 | 121 | month += 1 122 | } 123 | 124 | return DateTime(seconds: Int32(sec), minutes: Int32(min), hours: Int32(hour), dayOfMonth: Int32(mday), month: Int32(month - 1), year: Int32(year - 1900), dayOfWeek: Int32(wday), dayOfYear: Int32(yday)) 125 | } 126 | -------------------------------------------------------------------------------- /Extensions/UIKit+Extensions.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | 4 | extension UIColor { 5 | 6 | public convenience init(rgb value: UInt) { 7 | self.init(byteRed: UInt8((value >> 16) & 0xff), 8 | green: UInt8((value >> 8) & 0xff), 9 | blue: UInt8(value & 0xff), 10 | alpha: 0xff) 11 | } 12 | 13 | public convenience init(rgba value: UInt) { 14 | self.init(byteRed: UInt8((value >> 24) & 0xff), 15 | green: UInt8((value >> 16) & 0xff), 16 | blue: UInt8((value >> 8) & 0xff), 17 | alpha: UInt8(value & 0xff)) 18 | } 19 | 20 | public convenience init(byteRed red: UInt8, green: UInt8, blue: UInt8, alpha: UInt8 = 0xff) { 21 | self.init(red: CGFloat(red) / 255.0, 22 | green: CGFloat(green) / 255.0, 23 | blue: CGFloat(blue) / 255.0, 24 | alpha: CGFloat(alpha) / 255.0) 25 | } 26 | 27 | public static var random: UIColor { 28 | return UIColor(red: randComponent(), green: randComponent(), blue: randComponent(), alpha: 1.0) 29 | } 30 | 31 | public static func randComponent() -> CGFloat { 32 | return CGFloat(arc4random()) / CGFloat(UInt32.max) 33 | } 34 | } 35 | 36 | 37 | extension UIEdgeInsets { 38 | 39 | public static func from(edge: UIRectEdge, inset: CGFloat) -> UIEdgeInsets { 40 | let top: CGFloat = edge.contains(.top) ? inset : 0.0 41 | let left: CGFloat = edge.contains(.left) ? inset : 0.0 42 | let bottom: CGFloat = edge.contains(.bottom) ? inset : 0.0 43 | let right: CGFloat = edge.contains(.right) ? inset : 0.0 44 | return UIEdgeInsets(top: top, left: left, bottom: bottom, right: right) 45 | } 46 | 47 | public static func except(edge: UIRectEdge, inset: CGFloat) -> UIEdgeInsets { 48 | let top: CGFloat = edge.contains(.top) ? 0.0 : inset 49 | let left: CGFloat = edge.contains(.left) ? 0.0 : inset 50 | let bottom: CGFloat = edge.contains(.bottom) ? 0.0 : inset 51 | let right: CGFloat = edge.contains(.right) ? 0.0 : inset 52 | return UIEdgeInsets(top: top, left: left, bottom: bottom, right: right) 53 | } 54 | 55 | public static func left(_ inset: CGFloat) -> UIEdgeInsets { 56 | return UIEdgeInsets(top: 0.0, left: inset, bottom: 0.0, right: 0.0) 57 | } 58 | 59 | public static func right(_ inset: CGFloat) -> UIEdgeInsets { 60 | return UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: inset) 61 | } 62 | 63 | public static func top(_ inset: CGFloat) -> UIEdgeInsets { 64 | return UIEdgeInsets(top: inset, left: 0.0, bottom: 0.0, right: 0.0) 65 | } 66 | 67 | public static func bottom(_ inset: CGFloat) -> UIEdgeInsets { 68 | return UIEdgeInsets(top: 0.0, left: 0.0, bottom: inset, right: 0.0) 69 | } 70 | 71 | public static func all(_ value: CGFloat) -> UIEdgeInsets { 72 | return UIEdgeInsets(top: value, left: value, bottom: value, right: value) 73 | } 74 | 75 | public static func horizontal(_ value: CGFloat) -> UIEdgeInsets { 76 | return UIEdgeInsets(top: 0.0, left: value, bottom: 0.0, right: value) 77 | } 78 | 79 | public static func vertical(_ value: CGFloat) -> UIEdgeInsets { 80 | return UIEdgeInsets(top: value, left: 0.0, bottom: value, right: 0.0) 81 | } 82 | 83 | } 84 | 85 | extension UIEdgeInsets { 86 | 87 | public func withTop(_ top: CGFloat) -> UIEdgeInsets { 88 | return UIEdgeInsets(top: top, left: self.left, bottom: self.bottom, right: self.right) 89 | } 90 | 91 | public func withLeft(_ left: CGFloat) -> UIEdgeInsets { 92 | return UIEdgeInsets(top: self.top, left: left, bottom: self.bottom, right: self.right) 93 | } 94 | 95 | public func withBottom(_ bottom: CGFloat) -> UIEdgeInsets { 96 | return UIEdgeInsets(top: self.top, left: self.left, bottom: bottom, right: self.right) 97 | } 98 | 99 | public func withRight(_ right: CGFloat) -> UIEdgeInsets { 100 | return UIEdgeInsets(top: self.top, left: self.left, bottom: self.bottom, right: right) 101 | } 102 | 103 | public func withHorizontal(_ horizontal: CGFloat) -> UIEdgeInsets { 104 | return UIEdgeInsets(top: self.top, left: horizontal, bottom: self.bottom, right: horizontal) 105 | } 106 | 107 | public func withVertical(_ vertical: CGFloat) -> UIEdgeInsets { 108 | return UIEdgeInsets(top: vertical, left: self.left, bottom: vertical, right: self.right) 109 | } 110 | 111 | public func addMargins(margin: CGFloat) -> UIEdgeInsets { 112 | return UIEdgeInsets( 113 | top: top + margin, 114 | left: left + margin, 115 | bottom: bottom + margin, 116 | right: right + margin 117 | ) 118 | } 119 | 120 | public func adding(insets: UIEdgeInsets) -> UIEdgeInsets { 121 | return UIEdgeInsets( 122 | top: top + insets.top, 123 | left: left + insets.left, 124 | bottom: bottom + insets.bottom, 125 | right: right + insets.right 126 | ) 127 | } 128 | 129 | } 130 | -------------------------------------------------------------------------------- /Extensions/Extensions+UIView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UIColor { 4 | static func rgb(red: CGFloat, green: CGFloat, blue: CGFloat) -> UIColor { 5 | return UIColor(red: red / 255, green: green / 255, blue: blue / 255, alpha: 1) 6 | } 7 | } 8 | 9 | struct AnchoredConstraints { 10 | var top, leading, bottom, trailing, width, height: NSLayoutConstraint? 11 | } 12 | 13 | extension UIView { 14 | @discardableResult 15 | func anchor(top: NSLayoutYAxisAnchor?, leading: NSLayoutXAxisAnchor?, bottom: NSLayoutYAxisAnchor?, trailing: NSLayoutXAxisAnchor?, padding: UIEdgeInsets = .zero, size: CGSize = .zero) -> AnchoredConstraints { 16 | 17 | translatesAutoresizingMaskIntoConstraints = false 18 | var anchoredConstraints = AnchoredConstraints() 19 | 20 | if let top = top { 21 | anchoredConstraints.top = topAnchor.constraint(equalTo: top, constant: padding.top) 22 | } 23 | 24 | if let leading = leading { 25 | anchoredConstraints.leading = leadingAnchor.constraint(equalTo: leading, constant: padding.left) 26 | } 27 | 28 | if let bottom = bottom { 29 | anchoredConstraints.bottom = bottomAnchor.constraint(equalTo: bottom, constant: -padding.bottom) 30 | } 31 | 32 | if let trailing = trailing { 33 | anchoredConstraints.trailing = trailingAnchor.constraint(equalTo: trailing, constant: -padding.right) 34 | } 35 | 36 | if size.width != 0 { 37 | anchoredConstraints.width = widthAnchor.constraint(equalToConstant: size.width) 38 | } 39 | 40 | if size.height != 0 { 41 | anchoredConstraints.height = heightAnchor.constraint(equalToConstant: size.height) 42 | } 43 | 44 | [anchoredConstraints.top, anchoredConstraints.leading, anchoredConstraints.bottom, anchoredConstraints.trailing, anchoredConstraints.width, anchoredConstraints.height].forEach{ $0?.isActive = true } 45 | 46 | return anchoredConstraints 47 | } 48 | 49 | func fillSuperview(padding: UIEdgeInsets = .zero) { 50 | translatesAutoresizingMaskIntoConstraints = false 51 | if let superviewTopAnchor = superview?.topAnchor { 52 | topAnchor.constraint(equalTo: superviewTopAnchor, constant: padding.top).isActive = true 53 | } 54 | 55 | if let superviewBottomAnchor = superview?.bottomAnchor { 56 | bottomAnchor.constraint(equalTo: superviewBottomAnchor, constant: -padding.bottom).isActive = true 57 | } 58 | 59 | if let superviewLeadingAnchor = superview?.leadingAnchor { 60 | leadingAnchor.constraint(equalTo: superviewLeadingAnchor, constant: padding.left).isActive = true 61 | } 62 | 63 | if let superviewTrailingAnchor = superview?.trailingAnchor { 64 | trailingAnchor.constraint(equalTo: superviewTrailingAnchor, constant: -padding.right).isActive = true 65 | } 66 | } 67 | 68 | func centerInSuperview(size: CGSize = .zero) { 69 | translatesAutoresizingMaskIntoConstraints = false 70 | if let superviewCenterXAnchor = superview?.centerXAnchor { 71 | centerXAnchor.constraint(equalTo: superviewCenterXAnchor).isActive = true 72 | } 73 | 74 | if let superviewCenterYAnchor = superview?.centerYAnchor { 75 | centerYAnchor.constraint(equalTo: superviewCenterYAnchor).isActive = true 76 | } 77 | 78 | if size.width != 0 { 79 | widthAnchor.constraint(equalToConstant: size.width).isActive = true 80 | } 81 | 82 | if size.height != 0 { 83 | heightAnchor.constraint(equalToConstant: size.height).isActive = true 84 | } 85 | } 86 | 87 | func constrainHeight(_ constant: CGFloat) { 88 | translatesAutoresizingMaskIntoConstraints = false 89 | heightAnchor.constraint(equalToConstant: constant).isActive = true 90 | } 91 | 92 | func constrainWidth(_ constant: CGFloat) { 93 | translatesAutoresizingMaskIntoConstraints = false 94 | widthAnchor.constraint(equalToConstant: constant).isActive = true 95 | } 96 | 97 | @discardableResult 98 | func stack(_ axis: NSLayoutConstraint.Axis = .vertical, views: UIView..., spacing: CGFloat = 0) -> UIStackView { 99 | let stackView = UIStackView(arrangedSubviews: views) 100 | stackView.axis = axis 101 | stackView.spacing = spacing 102 | addSubview(stackView) 103 | stackView.fillSuperview() 104 | return stackView 105 | } 106 | 107 | func setupShadow(opacity: Float = 0, radius: CGFloat = 0, offset: CGSize = .zero, color: UIColor = .black) { 108 | layer.shadowOpacity = opacity 109 | layer.shadowRadius = radius 110 | layer.shadowOffset = offset 111 | layer.shadowColor = color.cgColor 112 | } 113 | 114 | convenience init(backgroundColor: UIColor = .clear) { 115 | self.init(frame: .zero) 116 | self.backgroundColor = backgroundColor 117 | } 118 | 119 | } 120 | 121 | extension UIStackView { 122 | @discardableResult 123 | func withMargins(_ margins: UIEdgeInsets) -> UIStackView { 124 | layoutMargins = margins 125 | isLayoutMarginsRelativeArrangement = true 126 | return self 127 | } 128 | 129 | @discardableResult 130 | func padLeft(_ left: CGFloat) -> UIStackView { 131 | isLayoutMarginsRelativeArrangement = true 132 | layoutMargins.left = left 133 | return self 134 | } 135 | 136 | @discardableResult 137 | func padTop(_ top: CGFloat) -> UIStackView { 138 | isLayoutMarginsRelativeArrangement = true 139 | layoutMargins.top = top 140 | return self 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /Other/DateFormat.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | func stringForShortTimestamp(hours: Int32, minutes: Int32, dateTimeFormat: PresentationDateTimeFormat) -> String { 4 | switch dateTimeFormat.timeFormat { 5 | case .regular: 6 | let hourString: String 7 | if hours == 0 { 8 | hourString = "12" 9 | } else if hours > 12 { 10 | hourString = "\(hours - 12)" 11 | } else { 12 | hourString = "\(hours)" 13 | } 14 | 15 | let periodString: String 16 | if hours >= 12 { 17 | periodString = "PM" 18 | } else { 19 | periodString = "AM" 20 | } 21 | if minutes >= 10 { 22 | return "\(hourString):\(minutes) \(periodString)" 23 | } else { 24 | return "\(hourString):0\(minutes) \(periodString)" 25 | } 26 | case .military: 27 | return String(format: "%02d:%02d", arguments: [Int(hours), Int(minutes)]) 28 | } 29 | } 30 | 31 | func stringForMessageTimestamp(timestamp: Int32, dateTimeFormat: PresentationDateTimeFormat, local: Bool = true) -> String { 32 | var t = Int(timestamp) 33 | var timeinfo = tm() 34 | if local { 35 | localtime_r(&t, &timeinfo) 36 | } else { 37 | gmtime_r(&t, &timeinfo) 38 | } 39 | 40 | return stringForShortTimestamp(hours: timeinfo.tm_hour, minutes: timeinfo.tm_min, dateTimeFormat: dateTimeFormat) 41 | } 42 | 43 | func stringForFullDate(timestamp: Int32, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat) -> String { 44 | var t: time_t = Int(timestamp) 45 | var timeinfo = tm() 46 | localtime_r(&t, &timeinfo); 47 | 48 | switch timeinfo.tm_mon + 1 { 49 | case 1: 50 | return strings.Time_PreciseDate_m1("\(timeinfo.tm_mday)", "\(2000 + timeinfo.tm_year - 100)", stringForShortTimestamp(hours: Int32(timeinfo.tm_hour), minutes: Int32(timeinfo.tm_min), dateTimeFormat: dateTimeFormat)).0 51 | case 2: 52 | return strings.Time_PreciseDate_m2("\(timeinfo.tm_mday)", "\(2000 + timeinfo.tm_year - 100)", stringForShortTimestamp(hours: Int32(timeinfo.tm_hour), minutes: Int32(timeinfo.tm_min), dateTimeFormat: dateTimeFormat)).0 53 | case 3: 54 | return strings.Time_PreciseDate_m3("\(timeinfo.tm_mday)", "\(2000 + timeinfo.tm_year - 100)", stringForShortTimestamp(hours: Int32(timeinfo.tm_hour), minutes: Int32(timeinfo.tm_min), dateTimeFormat: dateTimeFormat)).0 55 | case 4: 56 | return strings.Time_PreciseDate_m4("\(timeinfo.tm_mday)", "\(2000 + timeinfo.tm_year - 100)", stringForShortTimestamp(hours: Int32(timeinfo.tm_hour), minutes: Int32(timeinfo.tm_min), dateTimeFormat: dateTimeFormat)).0 57 | case 5: 58 | return strings.Time_PreciseDate_m5("\(timeinfo.tm_mday)", "\(2000 + timeinfo.tm_year - 100)", stringForShortTimestamp(hours: Int32(timeinfo.tm_hour), minutes: Int32(timeinfo.tm_min), dateTimeFormat: dateTimeFormat)).0 59 | case 6: 60 | return strings.Time_PreciseDate_m6("\(timeinfo.tm_mday)", "\(2000 + timeinfo.tm_year - 100)", stringForShortTimestamp(hours: Int32(timeinfo.tm_hour), minutes: Int32(timeinfo.tm_min), dateTimeFormat: dateTimeFormat)).0 61 | case 7: 62 | return strings.Time_PreciseDate_m7("\(timeinfo.tm_mday)", "\(2000 + timeinfo.tm_year - 100)", stringForShortTimestamp(hours: Int32(timeinfo.tm_hour), minutes: Int32(timeinfo.tm_min), dateTimeFormat: dateTimeFormat)).0 63 | case 8: 64 | return strings.Time_PreciseDate_m8("\(timeinfo.tm_mday)", "\(2000 + timeinfo.tm_year - 100)", stringForShortTimestamp(hours: Int32(timeinfo.tm_hour), minutes: Int32(timeinfo.tm_min), dateTimeFormat: dateTimeFormat)).0 65 | case 9: 66 | return strings.Time_PreciseDate_m9("\(timeinfo.tm_mday)", "\(2000 + timeinfo.tm_year - 100)", stringForShortTimestamp(hours: Int32(timeinfo.tm_hour), minutes: Int32(timeinfo.tm_min), dateTimeFormat: dateTimeFormat)).0 67 | case 10: 68 | return strings.Time_PreciseDate_m10("\(timeinfo.tm_mday)", "\(2000 + timeinfo.tm_year - 100)", stringForShortTimestamp(hours: Int32(timeinfo.tm_hour), minutes: Int32(timeinfo.tm_min), dateTimeFormat: dateTimeFormat)).0 69 | case 11: 70 | return strings.Time_PreciseDate_m11("\(timeinfo.tm_mday)", "\(2000 + timeinfo.tm_year - 100)", stringForShortTimestamp(hours: Int32(timeinfo.tm_hour), minutes: Int32(timeinfo.tm_min), dateTimeFormat: dateTimeFormat)).0 71 | case 12: 72 | return strings.Time_PreciseDate_m12("\(timeinfo.tm_mday)", "\(2000 + timeinfo.tm_year - 100)", stringForShortTimestamp(hours: Int32(timeinfo.tm_hour), minutes: Int32(timeinfo.tm_min), dateTimeFormat: dateTimeFormat)).0 73 | default: 74 | return "" 75 | } 76 | } 77 | 78 | func stringForDate(timestamp: Int32, strings: PresentationStrings) -> String { 79 | let formatter = DateFormatter() 80 | formatter.timeStyle = .none 81 | formatter.dateStyle = .medium 82 | formatter.timeZone = TimeZone(secondsFromGMT: 0) 83 | formatter.locale = localeWithStrings(strings) 84 | return formatter.string(from: Date(timeIntervalSince1970: Double(timestamp))) 85 | } 86 | 87 | func stringForDateWithoutYear(date: Date, strings: PresentationStrings) -> String { 88 | let formatter = DateFormatter() 89 | formatter.timeStyle = .none 90 | formatter.timeZone = TimeZone(secondsFromGMT: 0) 91 | formatter.locale = localeWithStrings(strings) 92 | formatter.setLocalizedDateFormatFromTemplate("MMMMd") 93 | return formatter.string(from: date) 94 | } 95 | 96 | func roundDateToDays(_ timestamp: Int32) -> Int32 { 97 | var calendar = Calendar(identifier: .gregorian) 98 | calendar.timeZone = TimeZone(secondsFromGMT: 0)! 99 | var components = calendar.dateComponents(Set([.era, .year, .month, .day]), from: Date(timeIntervalSince1970: Double(timestamp))) 100 | components.hour = 0 101 | components.minute = 0 102 | components.second = 0 103 | 104 | guard let date = calendar.date(from: components) else { 105 | assertionFailure() 106 | return timestamp 107 | } 108 | return Int32(date.timeIntervalSince1970) 109 | } 110 | -------------------------------------------------------------------------------- /Other/DeviceMetrics.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | public enum DeviceMetrics: CaseIterable { 4 | case iPhone4 5 | case iPhone5 6 | case iPhone6 7 | case iPhone6Plus 8 | case iPhoneX 9 | case iPhoneXSMax 10 | case iPad 11 | case iPadPro10Inch 12 | case iPadPro11Inch 13 | case iPadPro 14 | case iPadPro3rdGen 15 | 16 | public static func forScreenSize(_ size: CGSize, hintHasOnScreenNavigation: Bool = false) -> DeviceMetrics? { 17 | let additionalSize = CGSize(width: size.width, height: size.height + 20.0) 18 | for device in DeviceMetrics.allCases { 19 | let width = device.screenSize.width 20 | let height = device.screenSize.height 21 | 22 | if ((size.width.isEqual(to: width) && size.height.isEqual(to: height)) || size.height.isEqual(to: width) && size.width.isEqual(to: height)) || ((additionalSize.width.isEqual(to: width) && additionalSize.height.isEqual(to: height)) || additionalSize.height.isEqual(to: width) && additionalSize.width.isEqual(to: height)) { 23 | if hintHasOnScreenNavigation && device.onScreenNavigationHeight(inLandscape: false) == nil { 24 | continue 25 | } 26 | return device 27 | } 28 | } 29 | 30 | 31 | return nil 32 | } 33 | 34 | var screenSize: CGSize { 35 | switch self { 36 | case .iPhone4: 37 | return CGSize(width: 320.0, height: 480.0) 38 | case .iPhone5: 39 | return CGSize(width: 320.0, height: 568.0) 40 | case .iPhone6: 41 | return CGSize(width: 375.0, height: 667.0) 42 | case .iPhone6Plus: 43 | return CGSize(width: 414.0, height: 736.0) 44 | case .iPhoneX: 45 | return CGSize(width: 375.0, height: 812.0) 46 | case .iPhoneXSMax: 47 | return CGSize(width: 414.0, height: 896.0) 48 | case .iPad: 49 | return CGSize(width: 768.0, height: 1024.0) 50 | case .iPadPro10Inch: 51 | return CGSize(width: 834.0, height: 1112.0) 52 | case .iPadPro11Inch: 53 | return CGSize(width: 834.0, height: 1194.0) 54 | case .iPadPro, .iPadPro3rdGen: 55 | return CGSize(width: 1024.0, height: 1366.0) 56 | } 57 | } 58 | 59 | func safeAreaInsets(inLandscape: Bool) -> UIEdgeInsets { 60 | switch self { 61 | case .iPhoneX, .iPhoneXSMax: 62 | return inLandscape ? UIEdgeInsets(top: 0.0, left: 44.0, bottom: 0.0, right: 44.0) : UIEdgeInsets(top: 44.0, left: 0.0, bottom: 0.0, right: 0.0) 63 | default: 64 | return UIEdgeInsets.zero 65 | } 66 | } 67 | 68 | func onScreenNavigationHeight(inLandscape: Bool) -> CGFloat? { 69 | switch self { 70 | case .iPhoneX, .iPhoneXSMax: 71 | return inLandscape ? 21.0 : 34.0 72 | case .iPadPro3rdGen, .iPadPro11Inch: 73 | return 21.0 74 | default: 75 | return nil 76 | } 77 | } 78 | 79 | var statusBarHeight: CGFloat { 80 | switch self { 81 | case .iPhoneX, .iPhoneXSMax: 82 | return 44.0 83 | case .iPadPro11Inch, .iPadPro3rdGen: 84 | return 24.0 85 | default: 86 | return 20.0 87 | } 88 | } 89 | 90 | public func standardInputHeight(inLandscape: Bool) -> CGFloat { 91 | if inLandscape { 92 | switch self { 93 | case .iPhone4, .iPhone5: 94 | return 162.0 95 | case .iPhone6, .iPhone6Plus: 96 | return 163.0 97 | case .iPhoneX, .iPhoneXSMax: 98 | return 172.0 99 | case .iPad, .iPadPro10Inch: 100 | return 348.0 101 | case .iPadPro11Inch: 102 | return 368.0 103 | case .iPadPro: 104 | return 421.0 105 | case .iPadPro3rdGen: 106 | return 441.0 107 | } 108 | } else { 109 | switch self { 110 | case .iPhone4, .iPhone5, .iPhone6: 111 | return 216.0 112 | case .iPhone6Plus: 113 | return 227.0 114 | case .iPhoneX: 115 | return 291.0 116 | case .iPhoneXSMax: 117 | return 302.0 118 | case .iPad, .iPadPro10Inch: 119 | return 263.0 120 | case .iPadPro11Inch: 121 | return 283.0 122 | case .iPadPro: 123 | return 328.0 124 | case .iPadPro3rdGen: 125 | return 348.0 126 | } 127 | } 128 | } 129 | 130 | func predictiveInputHeight(inLandscape: Bool) -> CGFloat { 131 | if inLandscape { 132 | switch self { 133 | case .iPhone4, .iPhone5, .iPhone6, .iPhone6Plus, .iPhoneX, .iPhoneXSMax: 134 | return 37.0 135 | case .iPad, .iPadPro10Inch, .iPadPro11Inch, .iPadPro, .iPadPro3rdGen: 136 | return 50.0 137 | } 138 | } else { 139 | switch self { 140 | case .iPhone4, .iPhone5: 141 | return 37.0 142 | case .iPhone6, .iPhone6Plus, .iPhoneX, .iPhoneXSMax: 143 | return 44.0 144 | case .iPad, .iPadPro10Inch, .iPadPro11Inch, .iPadPro, .iPadPro3rdGen: 145 | return 50.0 146 | } 147 | } 148 | } 149 | 150 | public func previewingContentSize(inLandscape: Bool) -> CGSize { 151 | let screenSize = self.screenSize 152 | if inLandscape { 153 | switch self { 154 | case .iPhone5: 155 | return CGSize(width: screenSize.height, height: screenSize.width - 10.0) 156 | case .iPhone6: 157 | return CGSize(width: screenSize.height, height: screenSize.width - 22.0) 158 | case .iPhone6Plus: 159 | return CGSize(width: screenSize.height, height: screenSize.width - 22.0) 160 | case .iPhoneX: 161 | return CGSize(width: screenSize.height, height: screenSize.width + 48.0) 162 | case .iPhoneXSMax: 163 | return CGSize(width: screenSize.height, height: screenSize.width - 30.0) 164 | default: 165 | return CGSize(width: screenSize.height, height: screenSize.width - 10.0) 166 | } 167 | } else { 168 | switch self { 169 | case .iPhone5: 170 | return CGSize(width: screenSize.width, height: screenSize.height - 50.0) 171 | case .iPhone6: 172 | return CGSize(width: screenSize.width, height: screenSize.height - 97.0) 173 | case .iPhone6Plus: 174 | return CGSize(width: screenSize.width, height: screenSize.height - 95.0) 175 | case .iPhoneX: 176 | return CGSize(width: screenSize.width, height: screenSize.height - 154.0) 177 | case .iPhoneXSMax: 178 | return CGSize(width: screenSize.width, height: screenSize.height - 84.0) 179 | default: 180 | return CGSize(width: screenSize.width, height: screenSize.height - 50.0) 181 | } 182 | } 183 | } 184 | } 185 | --------------------------------------------------------------------------------