Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 96abfee256 | |||
| 86a06411d7 | |||
| 8031f5a28f | |||
| fdd2ed5b7c | |||
| 5cabfab574 | |||
| 535b76fea0 | |||
| 5b0672ee60 |
+35
-16
@@ -36,6 +36,12 @@
|
||||
391DB7D6215317A20083B8C3 /* TouchInteraction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 391DB7D5215317A20083B8C3 /* TouchInteraction.swift */; };
|
||||
3948F8B92193A3000043FD2E /* PinnedHeaderFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3948F8B82193A3000043FD2E /* PinnedHeaderFlowLayout.swift */; };
|
||||
398EF3F919D8AFA5093B15156F27E353 /* AssociatedObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = F90027142D498AD8ABA5215ADC06AC80 /* AssociatedObject.swift */; };
|
||||
399D418F21AC8D1C00A0B1EA /* UIColor+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 399D418D21AC8D1200A0B1EA /* UIColor+Extensions.swift */; };
|
||||
399D419421AC8DCF00A0B1EA /* NSAttributedString+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 399D419021AC8DC300A0B1EA /* NSAttributedString+Extensions.swift */; };
|
||||
399D419521AC8DCF00A0B1EA /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 399D419121AC8DC300A0B1EA /* String+Extensions.swift */; };
|
||||
399D419921AC8E0D00A0B1EA /* VerticalAlignmentLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 399D419621AC8E0C00A0B1EA /* VerticalAlignmentLayout.swift */; };
|
||||
399D419A21AC8E0D00A0B1EA /* PaginationFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 399D419721AC8E0D00A0B1EA /* PaginationFlowLayout.swift */; };
|
||||
399D419B21AC8E0D00A0B1EA /* SnappingFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 399D419821AC8E0D00A0B1EA /* SnappingFlowLayout.swift */; };
|
||||
3C087E90B6D5631B925B524FBD183DA5 /* Associated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43A88C1BE5A50F7B80FB3A7EF1DD75D5 /* Associated.swift */; };
|
||||
3C593180EE447D73A5ADBA3AE8909897 /* InputVisibilityController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80C3D37E1C6C9537466FBA3F006E1A2D /* InputVisibilityController.swift */; };
|
||||
40077E1D3AAA704AC97E1AEDA22CF464 /* StatusBarAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C4EEBAEB1E1FDB6EDA95CFF0B7ECFA7 /* StatusBarAppearance.swift */; };
|
||||
@@ -70,9 +76,7 @@
|
||||
C1EA2BF250F881A8CE274F56698CA5ED /* Hapticable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 352DCA45D28C00C59D68C1E66313DA94 /* Hapticable.swift */; };
|
||||
C5B3A325A391083BCBE0225D0E492651 /* AlphaTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73F563779A49E6F8BABD9F7F096FAE38 /* AlphaTransition.swift */; };
|
||||
C980A8B4B7D46F918161A80D523F5B9A /* Comparable+ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DF39CDC694A39ABC8161159EFBF1969 /* Comparable+ext.swift */; };
|
||||
CA706B65DFD60F03EDC1773828AC4306 /* UIColor+ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 002CE560EE32DF3EA2939698D55CA6DC /* UIColor+ext.swift */; };
|
||||
CA834F710C7B8DE7E78756369DB8B082 /* DateUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD719201D9EBBBFBC9D36540F14DFA52 /* DateUtilities.swift */; };
|
||||
CAF36258A097CBCB956B2742329293F7 /* SwiftyColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55C7669A602A5ACC46EB4D109E37EBBD /* SwiftyColor.swift */; };
|
||||
D5136D58B70ED60743594DDA623340A6 /* Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B2707546E60D2905D4AF46DB62253F2 /* Result.swift */; };
|
||||
D7D7B6285FF6691B27A7D73FF196468A /* Bundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 325E227EFA7126E2C5DD6AD776413857 /* Bundle.swift */; };
|
||||
D91F78D2FCE584E9E3E945F8FDD0C80E /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6604A7D69453B4569E4E4827FB9155A9 /* Foundation.framework */; };
|
||||
@@ -100,7 +104,6 @@
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
002CE560EE32DF3EA2939698D55CA6DC /* UIColor+ext.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "UIColor+ext.swift"; sourceTree = "<group>"; };
|
||||
03B1B68A1207D9BA0DEC85B5917051B3 /* Pods-utopia_Example-acknowledgements.markdown */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; path = "Pods-utopia_Example-acknowledgements.markdown"; sourceTree = "<group>"; };
|
||||
04B8F1CF04C636E2B5F6107FCFE6A533 /* Snapshot.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = Snapshot.swift; sourceTree = "<group>"; };
|
||||
0B2707546E60D2905D4AF46DB62253F2 /* Result.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = Result.swift; sourceTree = "<group>"; };
|
||||
@@ -125,6 +128,12 @@
|
||||
391DB7D3215317990083B8C3 /* GestureRecognizerDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GestureRecognizerDelegate.swift; sourceTree = "<group>"; };
|
||||
391DB7D5215317A20083B8C3 /* TouchInteraction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TouchInteraction.swift; sourceTree = "<group>"; };
|
||||
3948F8B82193A3000043FD2E /* PinnedHeaderFlowLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PinnedHeaderFlowLayout.swift; sourceTree = "<group>"; };
|
||||
399D418D21AC8D1200A0B1EA /* UIColor+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+Extensions.swift"; sourceTree = "<group>"; };
|
||||
399D419021AC8DC300A0B1EA /* NSAttributedString+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSAttributedString+Extensions.swift"; sourceTree = "<group>"; };
|
||||
399D419121AC8DC300A0B1EA /* String+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = "<group>"; };
|
||||
399D419621AC8E0C00A0B1EA /* VerticalAlignmentLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VerticalAlignmentLayout.swift; sourceTree = "<group>"; };
|
||||
399D419721AC8E0D00A0B1EA /* PaginationFlowLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaginationFlowLayout.swift; sourceTree = "<group>"; };
|
||||
399D419821AC8E0D00A0B1EA /* SnappingFlowLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnappingFlowLayout.swift; sourceTree = "<group>"; };
|
||||
41498A7B2FB6C962AF2CAA1727FA4BEE /* UITableView+Reusable.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "UITableView+Reusable.swift"; sourceTree = "<group>"; };
|
||||
43A88C1BE5A50F7B80FB3A7EF1DD75D5 /* Associated.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = Associated.swift; sourceTree = "<group>"; };
|
||||
474B741D8D941D2368F81BD923718887 /* Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
@@ -136,7 +145,6 @@
|
||||
507F6CFD78D079631EF4C7414D722BE9 /* ScrollDismission.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ScrollDismission.swift; sourceTree = "<group>"; };
|
||||
51A7A173B2BB4912B56024A0AB99C624 /* UISearchUtilities.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = UISearchUtilities.swift; sourceTree = "<group>"; };
|
||||
53845654F739F2E9534B24DEC5A577AD /* SimpleError.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SimpleError.swift; sourceTree = "<group>"; };
|
||||
55C7669A602A5ACC46EB4D109E37EBBD /* SwiftyColor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwiftyColor.swift; sourceTree = "<group>"; };
|
||||
5C127A0077D1A43E0B1AF57D5C3E0EC4 /* Toggler.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = Toggler.swift; sourceTree = "<group>"; };
|
||||
655F593EC54CC0CC958E764E3A09282D /* ImageSettable.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ImageSettable.swift; sourceTree = "<group>"; };
|
||||
6604A7D69453B4569E4E4827FB9155A9 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS10.3.sdk/System/Library/Frameworks/Foundation.framework; sourceTree = DEVELOPER_DIR; };
|
||||
@@ -223,7 +231,6 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
655F593EC54CC0CC958E764E3A09282D /* ImageSettable.swift */,
|
||||
55C7669A602A5ACC46EB4D109E37EBBD /* SwiftyColor.swift */,
|
||||
485554E3BDF22424E17D04F2C017B0C9 /* SwiftyImage.swift */,
|
||||
496E51ADD4779D6AC8AEB0E4AC214241 /* SwiftyImageView.swift */,
|
||||
);
|
||||
@@ -272,6 +279,9 @@
|
||||
children = (
|
||||
3948F8B82193A3000043FD2E /* PinnedHeaderFlowLayout.swift */,
|
||||
391DB7CD215316AB0083B8C3 /* HorizontalAlignmentLayout.swift */,
|
||||
399D419721AC8E0D00A0B1EA /* PaginationFlowLayout.swift */,
|
||||
399D419821AC8E0D00A0B1EA /* SnappingFlowLayout.swift */,
|
||||
399D419621AC8E0C00A0B1EA /* VerticalAlignmentLayout.swift */,
|
||||
);
|
||||
path = CollectionLayout;
|
||||
sourceTree = "<group>";
|
||||
@@ -464,11 +474,13 @@
|
||||
3519B7CEB0403B5FAF2E8610A97B1501 /* Require.swift */,
|
||||
0B2707546E60D2905D4AF46DB62253F2 /* Result.swift */,
|
||||
507F6CFD78D079631EF4C7414D722BE9 /* ScrollDismission.swift */,
|
||||
399D419021AC8DC300A0B1EA /* NSAttributedString+Extensions.swift */,
|
||||
399D419121AC8DC300A0B1EA /* String+Extensions.swift */,
|
||||
399D418D21AC8D1200A0B1EA /* UIColor+Extensions.swift */,
|
||||
53845654F739F2E9534B24DEC5A577AD /* SimpleError.swift */,
|
||||
B7478349DD7D5B359C7BDCE6B82A1658 /* Then.swift */,
|
||||
85C649278AE9FEC1B162C60ABAAE5123 /* Time.swift */,
|
||||
FE5615B6D865408D6F06B2F71F6FA57D /* TimingFunction.swift */,
|
||||
002CE560EE32DF3EA2939698D55CA6DC /* UIColor+ext.swift */,
|
||||
);
|
||||
name = Utilities;
|
||||
path = utopia/Source/Utilities;
|
||||
@@ -607,22 +619,23 @@
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastSwiftUpdateCheck = 0930;
|
||||
LastUpgradeCheck = 0930;
|
||||
LastUpgradeCheck = 1020;
|
||||
TargetAttributes = {
|
||||
02B55224E15D80EA5F1C45D2650F46E3 = {
|
||||
LastSwiftMigration = 1000;
|
||||
LastSwiftMigration = 1020;
|
||||
};
|
||||
55FF8012DB818DE68BDEF0BC12B62C22 = {
|
||||
LastSwiftMigration = 1000;
|
||||
LastSwiftMigration = 1020;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 2D8E8EC45A3A1A1D94AE762CB5028504 /* Build configuration list for PBXProject "Pods" */;
|
||||
compatibilityVersion = "Xcode 3.2";
|
||||
developmentRegion = English;
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = 7DB346D0F39D3F0E887471402A8071AB;
|
||||
productRefGroup = 73E0200A5FD816721F62FE79BAD1BC01 /* Products */;
|
||||
@@ -643,6 +656,7 @@
|
||||
C5B3A325A391083BCBE0225D0E492651 /* AlphaTransition.swift in Sources */,
|
||||
3C087E90B6D5631B925B524FBD183DA5 /* Associated.swift in Sources */,
|
||||
398EF3F919D8AFA5093B15156F27E353 /* AssociatedObject.swift in Sources */,
|
||||
399D418F21AC8D1C00A0B1EA /* UIColor+Extensions.swift in Sources */,
|
||||
FBA69FEF5BF5077D1A63854B7286CC57 /* AsyncOperation.swift in Sources */,
|
||||
DC9BC229563C9D4D76C4ADD9962E181A /* BarButtons.swift in Sources */,
|
||||
D7D7B6285FF6691B27A7D73FF196468A /* Bundle.swift in Sources */,
|
||||
@@ -668,6 +682,7 @@
|
||||
5179A999CF3F2B5C221D379A978334B1 /* Optional+ext.swift in Sources */,
|
||||
F844B62463F01BDF65C1553CD6670CC2 /* Random.swift in Sources */,
|
||||
286B06BEE5CF5C9680D6C4E3F7C671D5 /* Reachability.swift in Sources */,
|
||||
399D419421AC8DCF00A0B1EA /* NSAttributedString+Extensions.swift in Sources */,
|
||||
9E1484918CFB62F8A4DBAFD2604BDBBC /* Require.swift in Sources */,
|
||||
D5136D58B70ED60743594DDA623340A6 /* Result.swift in Sources */,
|
||||
F40A972F66F6674CDEA0187860E8785B /* ScaleModalTransitioning.swift in Sources */,
|
||||
@@ -678,10 +693,10 @@
|
||||
35F36F6F8769CDC19189C6C44BECE03E /* SignalSubscription.swift in Sources */,
|
||||
25625A79AD94F64B1C05606994AEC409 /* SimpleError.swift in Sources */,
|
||||
29396B3ADC9D79BC235C37FC87898C0E /* Snapshot.swift in Sources */,
|
||||
399D419521AC8DCF00A0B1EA /* String+Extensions.swift in Sources */,
|
||||
40077E1D3AAA704AC97E1AEDA22CF464 /* StatusBarAppearance.swift in Sources */,
|
||||
A848BE440EAD8450C7B15BE5DB31BAE7 /* String+ext.swift in Sources */,
|
||||
09471D36F0C65BF373FE54C944C68820 /* StringValidators.swift in Sources */,
|
||||
CAF36258A097CBCB956B2742329293F7 /* SwiftyColor.swift in Sources */,
|
||||
269A0BE57E27A3DFB1063BB77EDE4935 /* SwiftyImage.swift in Sources */,
|
||||
556DBB6B22CB0D57352504204588353C /* SwiftyImageView.swift in Sources */,
|
||||
A3DA1CA55FB34B50280E9E24460A7C50 /* Then.swift in Sources */,
|
||||
@@ -696,7 +711,6 @@
|
||||
9052FD4E4A2BF4D796D2521EE47B2069 /* UIButton+layout.swift in Sources */,
|
||||
0BE22BEB5AE38CEC94E0A184C8946969 /* UICollectionView+Reusable.swift in Sources */,
|
||||
391DB7D0215316F50083B8C3 /* InteractiveDismiss.swift in Sources */,
|
||||
CA706B65DFD60F03EDC1773828AC4306 /* UIColor+ext.swift in Sources */,
|
||||
696FBE84EEB2A8F325E8D551903BF524 /* UIControl+Signals.swift in Sources */,
|
||||
02807C6CD1D68F6A8F1D5F5EDBC075F3 /* UIGestureRecognizer+ext.swift in Sources */,
|
||||
E5B2024891F21779A4AB81AB91A2A03D /* UIImage+ext.swift in Sources */,
|
||||
@@ -722,7 +736,10 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
3948F8B92193A3000043FD2E /* PinnedHeaderFlowLayout.swift in Sources */,
|
||||
399D419A21AC8E0D00A0B1EA /* PaginationFlowLayout.swift in Sources */,
|
||||
399D419921AC8E0D00A0B1EA /* VerticalAlignmentLayout.swift in Sources */,
|
||||
5B190B9D09C4A639DA32D00681E1B4A7 /* Pods-utopia_Example-dummy.m in Sources */,
|
||||
399D419B21AC8E0D00A0B1EA /* SnappingFlowLayout.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -768,7 +785,7 @@
|
||||
SDKROOT = iphoneos;
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
|
||||
SWIFT_VERSION = 4.2;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
@@ -802,7 +819,7 @@
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) ";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 4.2;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
VERSION_INFO_PREFIX = "";
|
||||
@@ -813,6 +830,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
@@ -876,6 +894,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
@@ -956,7 +975,7 @@
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) ";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
|
||||
SWIFT_VERSION = 4.2;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
@@ -995,7 +1014,7 @@
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 4.2;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
VERSION_INFO_PREFIX = "";
|
||||
|
||||
@@ -41,7 +41,7 @@ extension KeyedDecodingContainer {
|
||||
}
|
||||
|
||||
public func decodeSafelyIfPresent<T: Decodable>(_ type: T.Type, forKey key: KeyedDecodingContainer.Key) -> T? {
|
||||
let decoded = try? decodeIfPresent(Safe<T>.self, forKey: key)
|
||||
let decoded = ((try? decodeIfPresent(Safe<T>.self, forKey: key)) as Safe<T>??)
|
||||
return decoded??.value
|
||||
}
|
||||
}
|
||||
@@ -67,10 +67,10 @@ public struct Id<Entity>: Hashable {
|
||||
self.raw = raw
|
||||
}
|
||||
|
||||
public var hashValue: Int {
|
||||
return raw.hashValue
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(raw.hashValue)
|
||||
}
|
||||
|
||||
|
||||
public static func ==(lhs: Id, rhs: Id) -> Bool {
|
||||
return lhs.raw == rhs.raw
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import UIKit
|
||||
|
||||
|
||||
public extension UIView {
|
||||
public var backgroundImage: UIImage? {
|
||||
var backgroundImage: UIImage? {
|
||||
get {
|
||||
guard let obj = layer.contents else { return nil }
|
||||
return UIImage(cgImage: obj as! CGImage)
|
||||
@@ -21,8 +21,8 @@ public extension UIView {
|
||||
|
||||
|
||||
public extension UIView {
|
||||
public final func updateOpaque() {
|
||||
if let color = backgroundColor, color.alphaValue == 1.0, alpha == 1.0 {
|
||||
final func updateOpaque() {
|
||||
if let color = backgroundColor, color.rgba.a == 1.0, alpha == 1.0 {
|
||||
isOpaque = true
|
||||
}
|
||||
else {
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import UIKit
|
||||
|
||||
public extension UIColor
|
||||
{
|
||||
public final var redValue: CGFloat { return rgba().r }
|
||||
public final var greenValue: CGFloat { return rgba().g }
|
||||
public final var blueValue: CGFloat { return rgba().b }
|
||||
public final var alphaValue: CGFloat { return rgba().a }
|
||||
|
||||
private final func rgba() -> (r: CGFloat, g: CGFloat, b: CGFloat, a: CGFloat)
|
||||
{
|
||||
guard let components: [CGFloat] = cgColor.components else { return (0, 0, 0, 0) }
|
||||
let numberOfComponents: Int = cgColor.numberOfComponents
|
||||
switch numberOfComponents {
|
||||
case 4:
|
||||
return (components[0], components[1], components[2], components[3])
|
||||
case 2:
|
||||
return (components[0], components[0], components[0], components[1])
|
||||
default:
|
||||
return (0, 0, 0, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,12 @@ import UIKit
|
||||
|
||||
public extension UIImage
|
||||
{
|
||||
public final var isOpaque: Bool {
|
||||
final var isOpaque: Bool {
|
||||
let alphaInfo = cgImage?.alphaInfo
|
||||
return !(alphaInfo == .first || alphaInfo == .last || alphaInfo == .premultipliedFirst || alphaInfo == .premultipliedLast)
|
||||
}
|
||||
|
||||
public final func reSize(to size: CGSize) -> UIImage {
|
||||
final func reSize(to size: CGSize) -> UIImage {
|
||||
guard size.width > 0 && size.height > 0 else { return self }
|
||||
|
||||
UIGraphicsBeginImageContextWithOptions(size, isOpaque, 0.0)
|
||||
@@ -18,7 +18,7 @@ public extension UIImage
|
||||
return scaledImage
|
||||
}
|
||||
|
||||
public final func reSize(toFit size: CGSize) -> UIImage {
|
||||
final func reSize(toFit size: CGSize) -> UIImage {
|
||||
guard size.width > 0 && size.height > 0 else { return self }
|
||||
|
||||
let imageAspectRatio = self.size.width / self.size.height
|
||||
@@ -40,7 +40,7 @@ public extension UIImage
|
||||
return scaledImage
|
||||
}
|
||||
|
||||
public final func reSize(toFill size: CGSize) -> UIImage {
|
||||
final func reSize(toFill size: CGSize) -> UIImage {
|
||||
guard size.width > 0 && size.height > 0 else { return self }
|
||||
|
||||
let imageAspectRatio = self.size.width / self.size.height
|
||||
@@ -62,7 +62,7 @@ public extension UIImage
|
||||
return scaledImage
|
||||
}
|
||||
|
||||
public final func rounded(withCornerRadius radius: CGFloat, divideRadiusByImageScale: Bool = false) -> UIImage {
|
||||
final func rounded(withCornerRadius radius: CGFloat, divideRadiusByImageScale: Bool = false) -> UIImage {
|
||||
UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
|
||||
|
||||
let scaledRadius = divideRadiusByImageScale ? radius / scale : radius
|
||||
@@ -78,7 +78,7 @@ public extension UIImage
|
||||
return roundedImage
|
||||
}
|
||||
|
||||
public final func roundedIntoCircle() -> UIImage {
|
||||
final func roundedIntoCircle() -> UIImage {
|
||||
let radius = min(size.width, size.height) / 2.0
|
||||
var squareImage = self
|
||||
if size.width != size.height {
|
||||
|
||||
@@ -26,7 +26,7 @@ public class SwiftyImageView: UIView {
|
||||
|
||||
public extension SwiftyImageView {
|
||||
|
||||
public enum ImageTransition {
|
||||
enum ImageTransition {
|
||||
case noTransition
|
||||
case crossDissolve(TimeInterval)
|
||||
case curlDown(TimeInterval)
|
||||
@@ -109,7 +109,7 @@ public extension SwiftyImageView {
|
||||
}
|
||||
}
|
||||
|
||||
public final func transition(_ imageTransition: ImageTransition, with image: UIImage) {
|
||||
final func transition(_ imageTransition: ImageTransition, with image: UIImage) {
|
||||
|
||||
UIView.transition(with: self, duration: imageTransition.duration, options: imageTransition.animationOptions, animations: { imageTransition.animations(self, image) }, completion: imageTransition.completion)
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import UIKit
|
||||
/// Extends UIBarButtonItem with signal for the action.
|
||||
public extension UIBarButtonItem {
|
||||
/// A signal that fires for each action event.
|
||||
public var onAction: Signal<(Void)> {
|
||||
var onAction: Signal<(Void)> {
|
||||
return getOrCreateSignal();
|
||||
}
|
||||
|
||||
|
||||
@@ -9,77 +9,77 @@ import UIKit
|
||||
/// Extends UIControl with signals for all ui control events.
|
||||
public extension UIControl {
|
||||
/// A signal that fires for each touch down control event.
|
||||
public var onTouchDown: Signal<(Void)> {
|
||||
var onTouchDown: Signal<(Void)> {
|
||||
return getOrCreateSignalForUIControlEvent(.touchDown);
|
||||
}
|
||||
|
||||
/// A signal that fires for each touch down repeat control event.
|
||||
public var onTouchDownRepeat: Signal<(Void)> {
|
||||
var onTouchDownRepeat: Signal<(Void)> {
|
||||
return getOrCreateSignalForUIControlEvent(.touchDownRepeat);
|
||||
}
|
||||
|
||||
/// A signal that fires for each touch drag inside control event.
|
||||
public var onTouchDragInside: Signal<(Void)> {
|
||||
var onTouchDragInside: Signal<(Void)> {
|
||||
return getOrCreateSignalForUIControlEvent(.touchDragInside);
|
||||
}
|
||||
|
||||
/// A signal that fires for each touch drag outside control event.
|
||||
public var onTouchDragOutside: Signal<(Void)> {
|
||||
var onTouchDragOutside: Signal<(Void)> {
|
||||
return getOrCreateSignalForUIControlEvent(.touchDragOutside);
|
||||
}
|
||||
|
||||
/// A signal that fires for each touch drag enter control event.
|
||||
public var onTouchDragEnter: Signal<(Void)> {
|
||||
var onTouchDragEnter: Signal<(Void)> {
|
||||
return getOrCreateSignalForUIControlEvent(.touchDragEnter);
|
||||
}
|
||||
|
||||
/// A signal that fires for each touch drag exit control event.
|
||||
public var onTouchDragExit: Signal<(Void)> {
|
||||
var onTouchDragExit: Signal<(Void)> {
|
||||
return getOrCreateSignalForUIControlEvent(.touchDragExit);
|
||||
}
|
||||
|
||||
/// A signal that fires for each touch up inside control event.
|
||||
public var onTouchUpInside: Signal<(Void)> {
|
||||
var onTouchUpInside: Signal<(Void)> {
|
||||
return getOrCreateSignalForUIControlEvent(.touchUpInside);
|
||||
}
|
||||
|
||||
/// A signal that fires for each primary action control event.
|
||||
public var onPrimaryActionTriggered: Signal<(Void)> {
|
||||
var onPrimaryActionTriggered: Signal<(Void)> {
|
||||
return getOrCreateSignalForUIControlEvent(.primaryActionTriggered);
|
||||
}
|
||||
|
||||
/// A signal that fires for each touch up outside control event.
|
||||
public var onTouchUpOutside: Signal<(Void)> {
|
||||
var onTouchUpOutside: Signal<(Void)> {
|
||||
return getOrCreateSignalForUIControlEvent(.touchUpOutside);
|
||||
}
|
||||
|
||||
/// A signal that fires for each touch cancel control event.
|
||||
public var onTouchCancel: Signal<(Void)> {
|
||||
var onTouchCancel: Signal<(Void)> {
|
||||
return getOrCreateSignalForUIControlEvent(.touchCancel);
|
||||
}
|
||||
|
||||
/// A signal that fires for each value changed control event.
|
||||
public var onValueChanged: Signal<(Void)> {
|
||||
var onValueChanged: Signal<(Void)> {
|
||||
return getOrCreateSignalForUIControlEvent(.valueChanged);
|
||||
}
|
||||
|
||||
/// A signal that fires for each editing did begin control event.
|
||||
public var onEditingDidBegin: Signal<(Void)> {
|
||||
var onEditingDidBegin: Signal<(Void)> {
|
||||
return getOrCreateSignalForUIControlEvent(.editingDidBegin);
|
||||
}
|
||||
|
||||
/// A signal that fires for each editing changed control event.
|
||||
public var onEditingChanged: Signal<(Void)> {
|
||||
var onEditingChanged: Signal<(Void)> {
|
||||
return getOrCreateSignalForUIControlEvent(.editingChanged);
|
||||
}
|
||||
|
||||
/// A signal that fires for each editing did end control event.
|
||||
public var onEditingDidEnd: Signal<(Void)> {
|
||||
var onEditingDidEnd: Signal<(Void)> {
|
||||
return getOrCreateSignalForUIControlEvent(.editingDidEnd);
|
||||
}
|
||||
|
||||
/// A signal that fires for each editing did end on exit control event.
|
||||
public var onEditingDidEndOnExit: Signal<(Void)> {
|
||||
var onEditingDidEndOnExit: Signal<(Void)> {
|
||||
return getOrCreateSignalForUIControlEvent(.editingDidEndOnExit);
|
||||
}
|
||||
|
||||
|
||||
@@ -80,7 +80,7 @@ public extension MutableCollection where Index == Int {
|
||||
Returns a random element from the collection.
|
||||
- returns: A random element from the collection.
|
||||
*/
|
||||
public func random() -> Iterator.Element {
|
||||
func random() -> Iterator.Element {
|
||||
let index = Int(arc4random_uniform(UInt32(count)))
|
||||
return self[index]
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import CoreGraphics
|
||||
public extension Int {
|
||||
|
||||
/// Returns a random Int point number between 0 and Int.max.
|
||||
public static var random: Int {
|
||||
static var random: Int {
|
||||
return Int.random(n: Int.max)
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ public extension Int {
|
||||
///
|
||||
/// - Parameter n: Interval max
|
||||
/// - Returns: Returns a random Int point number between 0 and n max
|
||||
public static func random(n: Int) -> Int {
|
||||
static func random(n: Int) -> Int {
|
||||
return Int(arc4random_uniform(UInt32(n)))
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ public extension Int {
|
||||
/// - min: Interval minimun
|
||||
/// - max: Interval max
|
||||
/// - Returns: Returns a random Int point number between 0 and n max
|
||||
public static func random(min: Int, max: Int) -> Int {
|
||||
static func random(min: Int, max: Int) -> Int {
|
||||
return Int.random(n: max - min + 1) + min
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,7 @@ public extension Int {
|
||||
public extension Double {
|
||||
|
||||
/// Returns a random floating point number between 0.0 and 1.0, inclusive.
|
||||
public static var random: Double {
|
||||
static var random: Double {
|
||||
return Double(arc4random()) / 0xFFFFFFFF
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ public extension Double {
|
||||
///
|
||||
/// - Parameter n: Interval max
|
||||
/// - Returns: Returns a random double point number between 0 and n max
|
||||
public static func random(min: Double, max: Double) -> Double {
|
||||
static func random(min: Double, max: Double) -> Double {
|
||||
return Double.random * (max - min) + min
|
||||
}
|
||||
}
|
||||
@@ -50,15 +50,15 @@ public extension Double {
|
||||
public extension Float {
|
||||
|
||||
/// Returns a random floating point number between 0.0 and 1.0, inclusive.
|
||||
public static var random: Float {
|
||||
return Float(arc4random()) / 0xFFFFFFFF
|
||||
static var random: Float {
|
||||
return Float(arc4random()) / 4294967296
|
||||
}
|
||||
|
||||
/// Random float between 0 and n-1.
|
||||
///
|
||||
/// - Parameter n: Interval max
|
||||
/// - Returns: Returns a random float point number between 0 and n max
|
||||
public static func random(min: Float, max: Float) -> Float {
|
||||
static func random(min: Float, max: Float) -> Float {
|
||||
return Float.random * (max - min) + min
|
||||
}
|
||||
}
|
||||
@@ -68,12 +68,12 @@ public extension Float {
|
||||
public extension CGFloat {
|
||||
|
||||
/// Randomly returns either 1.0 or -1.0.
|
||||
public static var randomSign: CGFloat {
|
||||
static var randomSign: CGFloat {
|
||||
return (arc4random_uniform(2) == 0) ? 1.0 : -1.0
|
||||
}
|
||||
|
||||
/// Returns a random floating point number between 0.0 and 1.0, inclusive.
|
||||
public static var random: CGFloat {
|
||||
static var random: CGFloat {
|
||||
return CGFloat(Float.random)
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ public extension CGFloat {
|
||||
///
|
||||
/// - Parameter n: Interval max
|
||||
/// - Returns: Returns a random CGFloat point number between 0 and n max
|
||||
public static func random(min: CGFloat, max: CGFloat) -> CGFloat {
|
||||
static func random(min: CGFloat, max: CGFloat) -> CGFloat {
|
||||
return CGFloat.random * (max - min) + min
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,8 @@ public let none = None()
|
||||
public struct None {}
|
||||
|
||||
extension None: Hashable {
|
||||
public var hashValue: Int {
|
||||
return 0
|
||||
}
|
||||
|
||||
public func hash(into hasher: inout Hasher) { }
|
||||
}
|
||||
|
||||
public func == (lhs: None, rhs: None) -> Bool { return true }
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import UIKit
|
||||
|
||||
public class PaginationFlowLayout: UICollectionViewFlowLayout {
|
||||
|
||||
override public func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
|
||||
|
||||
guard let collectionView = collectionView,
|
||||
let layoutAttributes: Array = layoutAttributesForElements(in: collectionView.bounds),
|
||||
layoutAttributes.count != 0 else {
|
||||
return proposedContentOffset
|
||||
}
|
||||
|
||||
var firstAttribute: UICollectionViewLayoutAttributes = layoutAttributes[0]
|
||||
for attribute: UICollectionViewLayoutAttributes in layoutAttributes {
|
||||
guard attribute.representedElementCategory == .cell else { continue }
|
||||
|
||||
switch scrollDirection {
|
||||
case .horizontal:
|
||||
if((velocity.x > 0.0 && attribute.center.x > firstAttribute.center.x) ||
|
||||
(velocity.x <= 0.0 && attribute.center.x < firstAttribute.center.x)) {
|
||||
firstAttribute = attribute;
|
||||
}
|
||||
case .vertical:
|
||||
if((velocity.y > 0.0 && attribute.center.y > firstAttribute.center.y) ||
|
||||
(velocity.y <= 0.0 && attribute.center.y < firstAttribute.center.y)) {
|
||||
firstAttribute = attribute;
|
||||
}
|
||||
@unknown default:
|
||||
fatalError()
|
||||
}
|
||||
}
|
||||
|
||||
switch scrollDirection {
|
||||
case .horizontal:
|
||||
return CGPoint(x: firstAttribute.center.x - collectionView.bounds.size.width * 0.5, y: proposedContentOffset.y)
|
||||
case .vertical:
|
||||
return CGPoint(x: proposedContentOffset.x, y: firstAttribute.center.y - collectionView.bounds.size.height * 0.5)
|
||||
@unknown default:
|
||||
fatalError()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
|
||||
final class SnappingFlowLayout: UICollectionViewFlowLayout {
|
||||
|
||||
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
|
||||
|
||||
guard let collectionView = collectionView else {
|
||||
return super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity)
|
||||
}
|
||||
|
||||
var offsetAdjustment = CGFloat.greatestFiniteMagnitude
|
||||
switch scrollDirection {
|
||||
case .horizontal:
|
||||
let horizontalOffset = proposedContentOffset.x + collectionView.contentInset.left
|
||||
|
||||
let targetRect = CGRect(x: proposedContentOffset.x, y: 0, width: collectionView.bounds.size.width, height: collectionView.bounds.size.height)
|
||||
|
||||
let layoutAttributesArray = super.layoutAttributesForElements(in: targetRect)
|
||||
|
||||
layoutAttributesArray?.forEach { layoutAttributes in
|
||||
let itemOffset = layoutAttributes.frame.origin.x
|
||||
if fabsf(Float(itemOffset - horizontalOffset)) < fabsf(Float(offsetAdjustment)) {
|
||||
offsetAdjustment = itemOffset - horizontalOffset
|
||||
}
|
||||
}
|
||||
let result = CGPoint(x: proposedContentOffset.x + offsetAdjustment, y: proposedContentOffset.y)
|
||||
return result
|
||||
|
||||
case .vertical:
|
||||
let verticalOffset = proposedContentOffset.y + collectionView.contentInset.top
|
||||
|
||||
let targetRect = CGRect(x: 0, y: proposedContentOffset.y, width: collectionView.bounds.size.width, height: collectionView.bounds.size.height)
|
||||
|
||||
let layoutAttributesArray = super.layoutAttributesForElements(in: targetRect)
|
||||
layoutAttributesArray?.forEach { layoutAttributes in
|
||||
let itemOffset = layoutAttributes.frame.origin.y
|
||||
if fabsf(Float(itemOffset - verticalOffset)) < fabsf(Float(offsetAdjustment)) {
|
||||
offsetAdjustment = itemOffset - verticalOffset
|
||||
}
|
||||
}
|
||||
|
||||
let result = CGPoint(x: proposedContentOffset.x, y: proposedContentOffset.y + offsetAdjustment)
|
||||
return result
|
||||
@unknown default:
|
||||
fatalError()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
public final class VerticalAlignmentLayout: UICollectionViewFlowLayout {
|
||||
|
||||
// KVO
|
||||
fileprivate struct KVO {
|
||||
|
||||
var keyPath: String
|
||||
var options: NSKeyValueObservingOptions
|
||||
var context: Int
|
||||
|
||||
static var contentOffset = KVO(
|
||||
keyPath: #keyPath(UICollectionViewFlowLayout.collectionView.contentOffset),
|
||||
options: .new,
|
||||
context: 0
|
||||
)
|
||||
|
||||
static var bounds = KVO(
|
||||
keyPath: #keyPath(UICollectionViewFlowLayout.collectionView.bounds),
|
||||
options: .new,
|
||||
context: 1
|
||||
)
|
||||
}
|
||||
|
||||
public enum Alignment {
|
||||
case top
|
||||
case center
|
||||
case bottom
|
||||
}
|
||||
|
||||
// Properties
|
||||
public var currentPage: Int?
|
||||
public var currentPageDidChange: (Int) -> Void = { _ in }
|
||||
let alignment: Alignment
|
||||
|
||||
var collectionHeight: CGFloat {
|
||||
guard let collection = collectionView else { return 0 }
|
||||
return collection.bounds.size.height - collection.contentInset.top - collection.contentInset.bottom
|
||||
}
|
||||
|
||||
deinit {
|
||||
removeObserver(self, forKeyPath: KVO.bounds.keyPath, context: &KVO.bounds.context)
|
||||
}
|
||||
|
||||
public init(itemSize: CGSize, spacing: CGFloat, alignment: Alignment) {
|
||||
self.alignment = alignment
|
||||
super.init()
|
||||
self.itemSize = itemSize
|
||||
self.scrollDirection = .vertical
|
||||
self.minimumLineSpacing = spacing
|
||||
|
||||
addObserver(self,
|
||||
forKeyPath: KVO.bounds.keyPath,
|
||||
options: KVO.bounds.options,
|
||||
context: &KVO.bounds.context)
|
||||
|
||||
addObserver(self,
|
||||
forKeyPath: KVO.contentOffset.keyPath,
|
||||
options: KVO.contentOffset.options,
|
||||
context: &KVO.contentOffset.context)
|
||||
}
|
||||
|
||||
public required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
}
|
||||
|
||||
// MARK: override
|
||||
extension VerticalAlignmentLayout {
|
||||
|
||||
public override func prepare() {
|
||||
switch alignment {
|
||||
case .top:
|
||||
sectionInset.top = 0
|
||||
sectionInset.bottom = collectionHeight - itemSize.height
|
||||
case .center:
|
||||
let inset = (collectionHeight - itemSize.height) / 2
|
||||
sectionInset.top = inset
|
||||
sectionInset.bottom = inset
|
||||
case .bottom:
|
||||
sectionInset.top = collectionHeight - itemSize.height
|
||||
sectionInset.bottom = 0
|
||||
}
|
||||
super.prepare()
|
||||
}
|
||||
|
||||
public override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
|
||||
|
||||
guard let collectionView = self.collectionView else {
|
||||
return proposedContentOffset
|
||||
}
|
||||
|
||||
let proposedRect = CGRect(x: proposedContentOffset.x, y: 0, width: collectionView.bounds.width, height: collectionView.bounds.height)
|
||||
|
||||
guard let layoutAttributes = self.layoutAttributesForElements(in: proposedRect) else {
|
||||
return proposedContentOffset
|
||||
}
|
||||
|
||||
var candidateAttributes: UICollectionViewLayoutAttributes?
|
||||
let proposedContentOffsetY: CGFloat
|
||||
switch alignment {
|
||||
case .top:
|
||||
proposedContentOffsetY = proposedContentOffset.y + itemSize.height / 2
|
||||
case .center:
|
||||
proposedContentOffsetY = proposedContentOffset.y + collectionView.bounds.height / 2
|
||||
case .bottom:
|
||||
proposedContentOffsetY = proposedContentOffset.y + collectionView.bounds.height
|
||||
}
|
||||
|
||||
for attributes in layoutAttributes {
|
||||
guard attributes.representedElementCategory == .cell else { continue }
|
||||
|
||||
if candidateAttributes == nil {
|
||||
candidateAttributes = attributes
|
||||
continue
|
||||
}
|
||||
|
||||
let attributePosition: CGFloat
|
||||
let candidatePosition: CGFloat
|
||||
switch alignment {
|
||||
case .top:
|
||||
attributePosition = attributes.frame.minY
|
||||
candidatePosition = candidateAttributes!.frame.minY
|
||||
case .center:
|
||||
attributePosition = attributes.center.y
|
||||
candidatePosition = candidateAttributes!.center.y
|
||||
case .bottom:
|
||||
attributePosition = attributes.frame.maxY
|
||||
candidatePosition = candidateAttributes!.frame.maxY
|
||||
}
|
||||
|
||||
if abs(attributePosition - proposedContentOffsetY) < abs(candidatePosition - proposedContentOffsetY) {
|
||||
candidateAttributes = attributes
|
||||
}
|
||||
}
|
||||
|
||||
guard let aCandidateAttributes = candidateAttributes else {
|
||||
return proposedContentOffset
|
||||
}
|
||||
|
||||
var newOffsetY: CGFloat
|
||||
switch alignment {
|
||||
case .top:
|
||||
newOffsetY = aCandidateAttributes.frame.minY - collectionView.contentInset.top
|
||||
case .center:
|
||||
newOffsetY = aCandidateAttributes.center.y - collectionView.bounds.size.height / 2
|
||||
case .bottom:
|
||||
newOffsetY = aCandidateAttributes.frame.minY - collectionView.bounds.size.height + itemSize.width
|
||||
}
|
||||
|
||||
let offset = newOffsetY - collectionView.contentOffset.y
|
||||
|
||||
if (velocity.y < 0 && offset > 0) || (velocity.y > 0 && offset < 0) {
|
||||
let pageHeight = itemSize.height + minimumLineSpacing
|
||||
newOffsetY += velocity.y > 0 ? pageHeight : -pageHeight
|
||||
}
|
||||
|
||||
return CGPoint(x: proposedContentOffset.x, y: newOffsetY)
|
||||
}
|
||||
|
||||
public override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
|
||||
switch context {
|
||||
case (&KVO.bounds.context)?:
|
||||
// TODO: Current layout makes hack for first item to appear in the center.
|
||||
// (method configure Insets). Settings insets shouldn't be done in invalidateLayout.
|
||||
// Layout should handle appear state on it's own.
|
||||
let oldValue = change?[.oldKey] as? NSValue
|
||||
let newValue = change?[.newKey] as? NSValue
|
||||
if oldValue?.cgRectValue != newValue?.cgRectValue {
|
||||
invalidateLayout()
|
||||
}
|
||||
|
||||
case (&KVO.contentOffset.context)?:
|
||||
guard let collectionView = collectionView, collectionView.frame.size != CGSize.zero else {
|
||||
return
|
||||
}
|
||||
|
||||
let visibleRect = CGRect(origin: collectionView.contentOffset, size: collectionView.frame.size)
|
||||
let layoutAttributes = layoutAttributesForElements(in: visibleRect)
|
||||
|
||||
let center: CGFloat
|
||||
switch alignment {
|
||||
case .top:
|
||||
let middle = collectionView.contentInset.top + itemSize.height / 2
|
||||
center = collectionView.contentOffset.y + middle
|
||||
case .center:
|
||||
let middle = collectionView.frame.height / 2
|
||||
center = collectionView.contentOffset.y + middle
|
||||
case .bottom:
|
||||
let middle = collectionView.frame.height - collectionView.contentInset.bottom - itemSize.height / 2
|
||||
center = collectionView.contentOffset.y + middle
|
||||
}
|
||||
|
||||
let closestAttribute = layoutAttributes?.sorted {
|
||||
abs($0.center.y - center) < abs($1.center.y - center)
|
||||
}.first
|
||||
if let closestAttribute = closestAttribute, currentPage != closestAttribute.indexPath.row {
|
||||
currentPage = closestAttribute.indexPath.row
|
||||
if let currentPage = currentPage {
|
||||
currentPageDidChange(currentPage)
|
||||
}
|
||||
}
|
||||
default:
|
||||
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import UIKit
|
||||
public extension CALayer {
|
||||
|
||||
@discardableResult
|
||||
public func add(to superlayer: CALayer) -> Self {
|
||||
func add(to superlayer: CALayer) -> Self {
|
||||
superlayer.addSublayer(self)
|
||||
return self
|
||||
}
|
||||
@@ -11,7 +11,7 @@ public extension CALayer {
|
||||
|
||||
public extension CATransaction {
|
||||
|
||||
public static func withoutActions(_ block: () -> Void) {
|
||||
static func withoutActions(_ block: () -> Void) {
|
||||
begin()
|
||||
setDisableActions(true)
|
||||
block()
|
||||
|
||||
@@ -1,11 +1,3 @@
|
||||
//
|
||||
// CoreGraphics+ext.swift
|
||||
// Vendefy
|
||||
//
|
||||
// Created by Dmitriy Kalachev on 4/8/18.
|
||||
// Copyright © 2018 Ramotion. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
public extension CGSize {
|
||||
@@ -14,13 +6,13 @@ public extension CGSize {
|
||||
self.init(width: value, height: value)
|
||||
}
|
||||
|
||||
init(_ width: CGFloat, _ height: CGFloat) {
|
||||
self.init(width: width, height: height)
|
||||
}
|
||||
|
||||
var asRect: CGRect {
|
||||
return CGRect(size: self)
|
||||
}
|
||||
|
||||
init(_ width: CGFloat, _ height: CGFloat) {
|
||||
self.init(width: width, height: height)
|
||||
}
|
||||
|
||||
func centered(in rect: CGRect) -> CGRect {
|
||||
var result = self.asRect
|
||||
@@ -29,14 +21,29 @@ public extension CGSize {
|
||||
return result
|
||||
}
|
||||
|
||||
func centered(in size: CGSize) -> CGRect {
|
||||
return centered(in: CGRect(origin: .zero, size: size))
|
||||
}
|
||||
|
||||
var asPixelsForMainScreen: CGSize {
|
||||
return self * UIScreen.main.scale
|
||||
}
|
||||
|
||||
var center: CGPoint {
|
||||
return CGPoint(x: width / 2, y: height / 2)
|
||||
}
|
||||
|
||||
static var greatestFiniteMagnitude: CGSize {
|
||||
return CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)
|
||||
}
|
||||
|
||||
var area: CGFloat {
|
||||
return width * height
|
||||
}
|
||||
}
|
||||
|
||||
public extension UIEdgeInsets {
|
||||
public init(value: CGFloat) {
|
||||
init(value: CGFloat) {
|
||||
self.init(top: value, left: value, bottom: value, right: value)
|
||||
}
|
||||
}
|
||||
@@ -59,6 +66,13 @@ public extension CGRect {
|
||||
return CGPoint(x: midX, y: midY)
|
||||
}
|
||||
|
||||
func insetBy(insets: UIEdgeInsets) -> CGRect {
|
||||
let x = origin.x + insets.left
|
||||
let y = origin.y + insets.top
|
||||
let w = size.width - insets.left - insets.right
|
||||
let h = size.height - insets.top - insets.bottom
|
||||
return CGRect(x: x, y: y, width: w, height: h)
|
||||
}
|
||||
}
|
||||
|
||||
public extension CGAffineTransform {
|
||||
@@ -66,7 +80,6 @@ public extension CGAffineTransform {
|
||||
init(scale: CGFloat) {
|
||||
self.init(scaleX: scale, y: scale)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public func + (lhs: CGPoint, rhs: CGPoint) -> CGPoint {
|
||||
|
||||
@@ -7,7 +7,7 @@ public extension UIButton {
|
||||
/// - bottom: title below button image
|
||||
/// - left: title to the left of button image
|
||||
/// - right: title to the right of button image
|
||||
public enum Position: Int {
|
||||
enum Position: Int {
|
||||
case top, bottom, left, right
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ public extension UIButton {
|
||||
/// - titlePosition: UIViewContentModeTop, UIViewContentModeBottom, UIViewContentModeLeft or UIViewContentModeRight
|
||||
/// - additionalSpacing: Spacing between image and title
|
||||
/// - state: State to apply this behaviour
|
||||
public func set(image: UIImage?, title: String, titlePosition: Position, spacing: CGFloat, state: UIControl.State){
|
||||
func set(image: UIImage?, title: String, titlePosition: Position, spacing: CGFloat, state: UIControl.State){
|
||||
imageView?.contentMode = .center
|
||||
setImage(image, for: state)
|
||||
setTitle(title, for: state)
|
||||
@@ -39,7 +39,7 @@ public extension UIButton {
|
||||
/// - titlePosition: UIViewContentModeTop, UIViewContentModeBottom, UIViewContentModeLeft or UIViewContentModeRight
|
||||
/// - additionalSpacing: Spacing between image and title
|
||||
/// - state: State to apply this behaviour
|
||||
public func set(image: UIImage?, attributedTitle title: NSAttributedString, titlePosition: Position, spacing: CGFloat, state: UIControl.State){
|
||||
func set(image: UIImage?, attributedTitle title: NSAttributedString, titlePosition: Position, spacing: CGFloat, state: UIControl.State){
|
||||
imageView?.contentMode = .center
|
||||
setImage(image, for: state)
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ public extension UINavigationController {
|
||||
}
|
||||
|
||||
func replace(_ vc: UIViewController, with replacementVC: UIViewController, animated: Bool = true) {
|
||||
guard let index = self.viewControllers.index(of: vc)
|
||||
guard let index = self.viewControllers.firstIndex(of: vc)
|
||||
else { return }
|
||||
|
||||
var viewControllers = self.viewControllers
|
||||
|
||||
@@ -3,31 +3,31 @@ import UIKit
|
||||
public extension UIView {
|
||||
|
||||
@discardableResult
|
||||
public func add(to superview: UIView) -> Self {
|
||||
func add(to superview: UIView) -> Self {
|
||||
superview.addSubview(self)
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func insert(to superview: UIView, at index: Int) -> Self {
|
||||
func insert(to superview: UIView, at index: Int) -> Self {
|
||||
superview.insertSubview(self, at: index)
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func insert(to superview: UIView, above view: UIView) -> Self {
|
||||
func insert(to superview: UIView, above view: UIView) -> Self {
|
||||
superview.insertSubview(self, aboveSubview: view)
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func insert(to superview: UIView, below view: UIView) -> Self {
|
||||
func insert(to superview: UIView, below view: UIView) -> Self {
|
||||
superview.insertSubview(self, belowSubview: view)
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func add(to stackview: UIStackView) -> Self {
|
||||
func add(to stackview: UIStackView) -> Self {
|
||||
stackview.addArrangedSubview(self)
|
||||
return self
|
||||
}
|
||||
|
||||
+4
-4
@@ -182,7 +182,7 @@ public extension UIView {
|
||||
static var viewExtension = "viewExtensionKeyboardVisibilityController"
|
||||
}
|
||||
|
||||
public var keyboardVisibilityController: InputVisibilityController? {
|
||||
var keyboardVisibilityController: InputVisibilityController? {
|
||||
get {
|
||||
return objc_getAssociatedObject(self, &KeyboardAssociatedKey.viewExtension) as? InputVisibilityController ?? nil
|
||||
}
|
||||
@@ -192,7 +192,7 @@ public extension UIView {
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func addInputVisibilityController() -> InputVisibilityController {
|
||||
func addInputVisibilityController() -> InputVisibilityController {
|
||||
var keyboardController = self.keyboardVisibilityController
|
||||
|
||||
if keyboardController == nil {
|
||||
@@ -211,14 +211,14 @@ public extension UIView {
|
||||
return keyboardVisibilityController!
|
||||
}
|
||||
|
||||
public func removeKeyboardVisibilityController() {
|
||||
func removeKeyboardVisibilityController() {
|
||||
keyboardVisibilityController = nil
|
||||
}
|
||||
}
|
||||
|
||||
public extension UIView {
|
||||
|
||||
public func findFirstResponder() -> UIView? {
|
||||
func findFirstResponder() -> UIView? {
|
||||
if isFirstResponder {
|
||||
return self
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import UIKit
|
||||
|
||||
public extension UIView {
|
||||
|
||||
public func snapshotImage(opaque: Bool = true, scale: CGFloat = UIScreen.main.scale * 2, afterScreenUpdates: Bool = false) -> UIImage? {
|
||||
func snapshotImage(opaque: Bool = true, scale: CGFloat = UIScreen.main.scale * 2, afterScreenUpdates: Bool = false) -> UIImage? {
|
||||
UIGraphicsBeginImageContextWithOptions(bounds.size, opaque, scale)
|
||||
drawHierarchy(in: bounds, afterScreenUpdates: afterScreenUpdates)
|
||||
let snapshotImage = UIGraphicsGetImageFromCurrentImageContext()
|
||||
@@ -12,7 +12,7 @@ public extension UIView {
|
||||
return snapshotImage
|
||||
}
|
||||
|
||||
public func snapshotView(opaque: Bool = true, scale: CGFloat = UIScreen.main.scale * 2, afterScreenUpdates: Bool = false) -> UIView? {
|
||||
func snapshotView(opaque: Bool = true, scale: CGFloat = UIScreen.main.scale * 2, afterScreenUpdates: Bool = false) -> UIView? {
|
||||
if let snapshotImage = snapshotImage(opaque: opaque, scale: scale, afterScreenUpdates: afterScreenUpdates) {
|
||||
return UIImageView(image: snapshotImage)
|
||||
} else {
|
||||
@@ -20,7 +20,7 @@ public extension UIView {
|
||||
}
|
||||
}
|
||||
|
||||
public func snapshotLayer(opaque: Bool = true, scale: CGFloat = UIScreen.main.scale * 2) -> UIImage? {
|
||||
func snapshotLayer(opaque: Bool = true, scale: CGFloat = UIScreen.main.scale * 2) -> UIImage? {
|
||||
|
||||
UIGraphicsBeginImageContextWithOptions(bounds.size, opaque, scale)
|
||||
guard let context = UIGraphicsGetCurrentContext() else { return nil }
|
||||
|
||||
@@ -2,10 +2,14 @@ import Foundation
|
||||
import UIKit
|
||||
|
||||
public protocol Togglable: class {
|
||||
var isOn: Bool { get }
|
||||
func selectedToggle(select: Bool)
|
||||
}
|
||||
|
||||
extension UIControl: Togglable {
|
||||
|
||||
public var isOn: Bool { return isSelected }
|
||||
|
||||
@objc public func selectedToggle(select: Bool) {
|
||||
isSelected = select
|
||||
}
|
||||
@@ -22,9 +26,13 @@ extension UISwitch {
|
||||
public struct Toggler {
|
||||
var togglers = [Togglable]()
|
||||
|
||||
public var index: Int {
|
||||
return togglers.firstIndex(where: { $0.isOn }) ?? 0
|
||||
}
|
||||
|
||||
public init(default index: Int = 0, togglers: [Togglable]) {
|
||||
self.togglers = togglers
|
||||
toggleControl(toggle: togglers[index], togglers: togglers)
|
||||
onAt(index: index)
|
||||
}
|
||||
|
||||
public func on(toggle: Togglable) {
|
||||
@@ -32,7 +40,9 @@ public struct Toggler {
|
||||
}
|
||||
|
||||
public func onAt(index: Int) {
|
||||
toggleControl(toggle: togglers[index], togglers: togglers)
|
||||
if let toggler = togglers.at(index) {
|
||||
toggleControl(toggle: toggler, togglers: togglers)
|
||||
}
|
||||
}
|
||||
|
||||
public mutating func add(toggle: Togglable) {
|
||||
|
||||
@@ -109,7 +109,20 @@ import UIKit
|
||||
setNeedsDisplay()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
setup()
|
||||
}
|
||||
|
||||
required public init?(coder aDecoder: NSCoder) {
|
||||
super.init(coder: aDecoder)
|
||||
setup()
|
||||
}
|
||||
|
||||
private func setup() {
|
||||
backgroundColor = .clear
|
||||
}
|
||||
|
||||
// MARK: - UIView
|
||||
override open func draw(_ rect: CGRect) {
|
||||
|
||||
@@ -2,19 +2,19 @@ import Foundation
|
||||
|
||||
public extension Bundle {
|
||||
|
||||
public var appName: String {
|
||||
var appName: String {
|
||||
return infoDictionary?["CFBundleName"] as! String
|
||||
}
|
||||
|
||||
public var bundleId: String {
|
||||
var bundleId: String {
|
||||
return bundleIdentifier!
|
||||
}
|
||||
|
||||
public var versionNumber: String {
|
||||
var versionNumber: String {
|
||||
return infoDictionary?["CFBundleShortVersionString"] as! String
|
||||
}
|
||||
|
||||
public var buildNumber: String {
|
||||
var buildNumber: String {
|
||||
return infoDictionary?["CFBundleVersion"] as! String
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,515 @@
|
||||
import Foundation
|
||||
|
||||
extension NSAttributedString {
|
||||
/**
|
||||
Returns a new mutable string with characters from a given character set removed.
|
||||
|
||||
See http://panupan.com/2012/06/04/trim-leading-and-trailing-whitespaces-from-nsmutableattributedstring/
|
||||
|
||||
- Parameters:
|
||||
- charSet: The character set with which to remove characters.
|
||||
- returns: A new string with the matching characters removed.
|
||||
*/
|
||||
public func trimmingCharacters(in set: CharacterSet) -> NSAttributedString {
|
||||
let modString = NSMutableAttributedString(attributedString: self)
|
||||
modString.trimCharacters(in: set)
|
||||
return NSAttributedString(attributedString: modString)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension NSMutableAttributedString {
|
||||
/**
|
||||
Modifies this instance of the string to remove characters from a given character set from
|
||||
the beginning and end of the string.
|
||||
|
||||
See http://panupan.com/2012/06/04/trim-leading-and-trailing-whitespaces-from-nsmutableattributedstring/
|
||||
|
||||
- Parameters:
|
||||
- charSet: The character set with which to remove characters.
|
||||
*/
|
||||
public func trimCharacters(in set: CharacterSet) {
|
||||
var range = (string as NSString).rangeOfCharacter(from: set)
|
||||
|
||||
// Trim leading characters from character set.
|
||||
while range.length != 0 && range.location == 0 {
|
||||
replaceCharacters(in: range, with: "")
|
||||
range = (string as NSString).rangeOfCharacter(from: set)
|
||||
}
|
||||
|
||||
// Trim trailing characters from character set.
|
||||
range = (string as NSString).rangeOfCharacter(from: set, options: .backwards)
|
||||
while range.length != 0 && NSMaxRange(range) == length {
|
||||
replaceCharacters(in: range, with: "")
|
||||
range = (string as NSString).rangeOfCharacter(from: set, options: .backwards)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
extension NSMutableAttributedString {
|
||||
|
||||
private var range: NSRange {
|
||||
return NSRange(location: 0, length: length)
|
||||
}
|
||||
|
||||
private var paragraphStyle: NSMutableParagraphStyle {
|
||||
let style = attributes(at: 0, effectiveRange: nil)[.paragraphStyle] as? NSMutableParagraphStyle
|
||||
return style ?? NSMutableParagraphStyle()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Font
|
||||
|
||||
extension NSMutableAttributedString {
|
||||
/**
|
||||
Applies a font to the entire string.
|
||||
|
||||
- parameter font: The font.
|
||||
*/
|
||||
@discardableResult
|
||||
public func font(_ font: UIFont) -> Self {
|
||||
if length > 0 {
|
||||
addAttribute(.font, value: font, range: range)
|
||||
}
|
||||
return self
|
||||
}
|
||||
|
||||
/**
|
||||
Applies a font to the entire string.
|
||||
|
||||
- parameter name: The font name.
|
||||
- parameter size: The font size.
|
||||
|
||||
Note: If the specified font name cannot be loaded, this method will fallback to the system font at the specified size.
|
||||
*/
|
||||
@discardableResult
|
||||
public func font(name: String, size: CGFloat) -> Self {
|
||||
if length > 0 {
|
||||
addAttribute(.font, value: UIFont(name: name, size: size) ?? .systemFont(ofSize: size), range: range)
|
||||
}
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Paragraph style
|
||||
|
||||
extension NSMutableAttributedString {
|
||||
|
||||
/**
|
||||
Applies a text alignment to the entire string.
|
||||
|
||||
- parameter alignment: The text alignment.
|
||||
*/
|
||||
@discardableResult
|
||||
public func alignment(_ alignment: NSTextAlignment) -> Self {
|
||||
if length > 0 {
|
||||
let paragraphStyle = self.paragraphStyle
|
||||
paragraphStyle.alignment = alignment
|
||||
|
||||
addAttribute(.paragraphStyle, value: paragraphStyle, range: range)
|
||||
}
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
/**
|
||||
Applies line spacing to the entire string.
|
||||
|
||||
- parameter lineSpacing: The line spacing amount.
|
||||
*/
|
||||
@discardableResult
|
||||
public func lineSpacing(_ lineSpacing: CGFloat) -> Self {
|
||||
if length > 0 {
|
||||
let paragraphStyle = self.paragraphStyle
|
||||
paragraphStyle.lineSpacing = lineSpacing
|
||||
|
||||
addAttribute(.paragraphStyle, value: paragraphStyle, range: range)
|
||||
}
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
/**
|
||||
Applies paragraph spacing to the entire string.
|
||||
|
||||
- parameter paragraphSpacing: The paragraph spacing amount.
|
||||
*/
|
||||
@discardableResult
|
||||
public func paragraphSpacing(_ paragraphSpacing: CGFloat) -> Self {
|
||||
if length > 0 {
|
||||
let paragraphStyle = self.paragraphStyle
|
||||
paragraphStyle.paragraphSpacing = paragraphSpacing
|
||||
|
||||
addAttribute(.paragraphStyle, value: paragraphStyle, range: range)
|
||||
}
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
/**
|
||||
Applies a line break mode to the entire string.
|
||||
|
||||
- parameter mode: The line break mode.
|
||||
*/
|
||||
@discardableResult
|
||||
public func lineBreak(_ mode: NSLineBreakMode) -> Self {
|
||||
if length > 0 {
|
||||
let paragraphStyle = self.paragraphStyle
|
||||
paragraphStyle.lineBreakMode = mode
|
||||
|
||||
addAttribute(.paragraphStyle, value: paragraphStyle, range: range)
|
||||
}
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
/**
|
||||
Applies a line height multiplier to the entire string.
|
||||
|
||||
- parameter multiple: The line height multiplier.
|
||||
*/
|
||||
@discardableResult
|
||||
public func lineHeight(multiple: CGFloat) -> Self {
|
||||
if length > 0 {
|
||||
let paragraphStyle = self.paragraphStyle
|
||||
paragraphStyle.lineHeightMultiple = multiple
|
||||
|
||||
addAttribute(.paragraphStyle, value: paragraphStyle, range: range)
|
||||
}
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
/**
|
||||
Applies a first line head indent to the string.
|
||||
|
||||
- parameter indent: The first line head indent amount.
|
||||
*/
|
||||
@discardableResult
|
||||
public func firstLineHeadIndent(_ indent: CGFloat) -> Self {
|
||||
if length > 0 {
|
||||
let paragraphStyle = self.paragraphStyle
|
||||
paragraphStyle.firstLineHeadIndent = indent
|
||||
|
||||
addAttribute(.paragraphStyle, value: paragraphStyle, range: range)
|
||||
}
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
/**
|
||||
Applies a head indent to the string.
|
||||
|
||||
- parameter indent: The head indent amount.
|
||||
*/
|
||||
@discardableResult
|
||||
public func headIndent(_ indent: CGFloat) -> Self {
|
||||
if length > 0 {
|
||||
let paragraphStyle = self.paragraphStyle
|
||||
paragraphStyle.headIndent = indent
|
||||
|
||||
addAttribute(.paragraphStyle, value: paragraphStyle, range: range)
|
||||
}
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
/**
|
||||
Applies a tail indent to the string.
|
||||
|
||||
- parameter indent: The tail indent amount.
|
||||
*/
|
||||
@discardableResult
|
||||
public func tailIndent(_ indent: CGFloat) -> Self {
|
||||
if length > 0 {
|
||||
let paragraphStyle = self.paragraphStyle
|
||||
paragraphStyle.tailIndent = indent
|
||||
|
||||
addAttribute(.paragraphStyle, value: paragraphStyle, range: range)
|
||||
}
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
/**
|
||||
Applies a minimum line height to the entire string.
|
||||
|
||||
- parameter height: The minimum line height.
|
||||
*/
|
||||
@discardableResult
|
||||
public func minimumLineHeight(_ height: CGFloat) -> Self {
|
||||
if length > 0 {
|
||||
let paragraphStyle = self.paragraphStyle
|
||||
paragraphStyle.minimumLineHeight = height
|
||||
|
||||
addAttribute(.paragraphStyle, value: paragraphStyle, range: range)
|
||||
}
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
/**
|
||||
Applies a maximum line height to the entire string.
|
||||
|
||||
- parameter height: The maximum line height.
|
||||
*/
|
||||
@discardableResult
|
||||
public func maximumLineHeight(_ height: CGFloat) -> Self {
|
||||
if length > 0 {
|
||||
let paragraphStyle = self.paragraphStyle
|
||||
paragraphStyle.maximumLineHeight = height
|
||||
|
||||
addAttribute(.paragraphStyle, value: paragraphStyle, range: range)
|
||||
}
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
/**
|
||||
Applies a base writing direction to the entire string.
|
||||
|
||||
- parameter direction: The base writing direction.
|
||||
*/
|
||||
@discardableResult
|
||||
public func baseWritingDirection(_ direction: NSWritingDirection) -> Self {
|
||||
if length > 0 {
|
||||
let paragraphStyle = self.paragraphStyle
|
||||
paragraphStyle.baseWritingDirection = direction
|
||||
|
||||
addAttribute(.paragraphStyle, value: paragraphStyle, range: range)
|
||||
}
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
/**
|
||||
Applies a paragraph spacing before amount to the string.
|
||||
|
||||
- parameter spacing: The distance between the paragraph’s top and the beginning of its text content.
|
||||
*/
|
||||
@discardableResult
|
||||
public func paragraphSpacingBefore(_ spacing: CGFloat) -> Self {
|
||||
if length > 0 {
|
||||
let paragraphStyle = self.paragraphStyle
|
||||
paragraphStyle.paragraphSpacingBefore = spacing
|
||||
|
||||
addAttribute(.paragraphStyle, value: paragraphStyle, range: range)
|
||||
}
|
||||
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Foreground color
|
||||
|
||||
extension NSMutableAttributedString {
|
||||
/**
|
||||
Applies the given color over the entire string, as the foreground color.
|
||||
|
||||
- parameter color: The color to apply.
|
||||
*/
|
||||
@discardableResult @nonobjc
|
||||
public func color(_ color: UIColor, alpha: CGFloat = 1) -> Self {
|
||||
if length > 0 {
|
||||
addAttribute(.foregroundColor, value: color.withAlphaComponent(alpha), range: range)
|
||||
}
|
||||
return self
|
||||
}
|
||||
|
||||
/**
|
||||
Applies the given color over the entire string, as the foreground color.
|
||||
|
||||
- parameter color: The color to apply.
|
||||
*/
|
||||
@discardableResult
|
||||
public func color(red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat = 1) -> Self {
|
||||
if length > 0 {
|
||||
addAttribute(.foregroundColor, value: UIColor(red: red, green: green, blue: blue, alpha: alpha), range: range)
|
||||
}
|
||||
return self
|
||||
}
|
||||
|
||||
/**
|
||||
Applies the given color over the entire string, as the foreground color.
|
||||
|
||||
- parameter color: The color to apply.
|
||||
*/
|
||||
@discardableResult
|
||||
public func color(white: CGFloat, alpha: CGFloat = 1) -> Self {
|
||||
if length > 0 {
|
||||
addAttribute(.foregroundColor, value: UIColor(white: white, alpha: alpha), range: range)
|
||||
}
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Underline, kern, strikethrough, stroke, shadow, text effect
|
||||
|
||||
extension NSMutableAttributedString {
|
||||
/**
|
||||
Applies a single underline under the entire string.
|
||||
|
||||
- parameter style: The `NSUnderlineStyle` to apply. Defaults to `.styleSingle`.
|
||||
*/
|
||||
@discardableResult
|
||||
public func underline(style: NSUnderlineStyle = .single, color: UIColor? = nil) -> Self {
|
||||
if length > 0 {
|
||||
addAttribute(.underlineStyle, value: style.rawValue, range: range)
|
||||
|
||||
if let color = color {
|
||||
addAttribute(.underlineColor, value: color, range: range)
|
||||
}
|
||||
}
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
/**
|
||||
Applies a kern (spacing) value to the entire string.
|
||||
|
||||
- parameter value: The space between each character in the string.
|
||||
*/
|
||||
@discardableResult
|
||||
public func kern(_ value: CGFloat) -> Self {
|
||||
if length > 0 {
|
||||
addAttribute(.kern, value: value, range: range)
|
||||
}
|
||||
return self
|
||||
}
|
||||
|
||||
/**
|
||||
Applies a strikethrough to the entire string.
|
||||
|
||||
- parameter style: The `NSUnderlineStyle` to apply. Defaults to `.styleSingle`.
|
||||
- parameter color: The underline color. Defaults to the color of the text.
|
||||
*/
|
||||
@discardableResult
|
||||
public func strikethrough(style: NSUnderlineStyle = .single, color: UIColor? = nil) -> Self {
|
||||
if length > 0 {
|
||||
addAttribute(.strikethroughStyle, value: style.rawValue, range: range)
|
||||
|
||||
if let color = color {
|
||||
addAttribute(.strikethroughColor, value: color, range: range)
|
||||
}
|
||||
}
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
/**
|
||||
Applies a stroke to the entire string.
|
||||
|
||||
- parameter color: The stroke color.
|
||||
- parameter width: The stroke width.
|
||||
*/
|
||||
@discardableResult
|
||||
public func stroke(color: UIColor, width: CGFloat) -> Self {
|
||||
if length > 0 {
|
||||
addAttributes([
|
||||
.strokeColor : color,
|
||||
.strokeWidth : width
|
||||
], range: range)
|
||||
}
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
/**
|
||||
Applies a shadow to the entire string.
|
||||
|
||||
- parameter color: The shadow color.
|
||||
- parameter radius: The shadow blur radius.
|
||||
- parameter offset: The shadow offset.
|
||||
*/
|
||||
@discardableResult
|
||||
public func shadow(color: UIColor, radius: CGFloat, offset: CGSize) -> Self {
|
||||
if length > 0 {
|
||||
let shadow = NSShadow()
|
||||
shadow.shadowColor = color
|
||||
shadow.shadowBlurRadius = radius
|
||||
shadow.shadowOffset = offset
|
||||
|
||||
addAttribute(.shadow, value: shadow, range: range)
|
||||
}
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Background color
|
||||
|
||||
extension NSMutableAttributedString {
|
||||
|
||||
/**
|
||||
Applies a background color to the entire string.
|
||||
|
||||
- parameter color: The color to apply.
|
||||
*/
|
||||
@discardableResult @nonobjc
|
||||
public func backgroundColor(_ color: UIColor, alpha: CGFloat = 1) -> Self {
|
||||
if length > 0 {
|
||||
addAttribute(.backgroundColor, value: color.withAlphaComponent(alpha), range: range)
|
||||
}
|
||||
return self
|
||||
}
|
||||
|
||||
/**
|
||||
Applies a background color to the entire string.
|
||||
|
||||
- parameter red: The red color component.
|
||||
- parameter green: The green color component.
|
||||
- parameter blue: The blue color component.
|
||||
- parameter alpha: The alpha component.
|
||||
*/
|
||||
@discardableResult
|
||||
public func backgroundColor(red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat = 1) -> Self {
|
||||
if length > 0 {
|
||||
addAttribute(.backgroundColor, value: UIColor(red: red, green: green, blue: blue, alpha: alpha), range: range)
|
||||
}
|
||||
return self
|
||||
}
|
||||
|
||||
/**
|
||||
Applies a background color to the entire string.
|
||||
|
||||
- parameter white: The white color component.
|
||||
- parameter alpha: The alpha component.
|
||||
*/
|
||||
@discardableResult
|
||||
public func backgroundColor(white: CGFloat, alpha: CGFloat = 1) -> Self {
|
||||
if length > 0 {
|
||||
addAttribute(.backgroundColor, value: UIColor(white: white, alpha: alpha), range: range)
|
||||
}
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
extension NSMutableAttributedString {
|
||||
|
||||
/**
|
||||
Applies a baseline offset to the entire string.
|
||||
|
||||
- parameter offset: The offset value.
|
||||
*/
|
||||
@discardableResult
|
||||
public func baselineOffset(_ offset: Float) -> Self {
|
||||
if length > 0 {
|
||||
addAttribute(.baselineOffset, value: NSNumber(value: offset), range: range)
|
||||
}
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
public func +(lhs: NSMutableAttributedString, rhs: NSAttributedString) -> NSMutableAttributedString {
|
||||
let lhs = NSMutableAttributedString(attributedString: lhs)
|
||||
lhs.append(rhs)
|
||||
return lhs
|
||||
}
|
||||
|
||||
public func +=(lhs: NSMutableAttributedString, rhs: NSAttributedString) {
|
||||
lhs.append(rhs)
|
||||
}
|
||||
@@ -80,7 +80,7 @@ public class Reachability {
|
||||
public var whenReachable: NetworkReachable?
|
||||
public var whenUnreachable: NetworkUnreachable?
|
||||
|
||||
@available(*, deprecated: 4.0, renamed: "allowsCellularConnection")
|
||||
@available(*, deprecated, renamed: "allowsCellularConnection")
|
||||
public let reachableOnWWAN: Bool = true
|
||||
|
||||
/// Set to `false` to force Reachability.connection to .none when on cellular connection (default value `true`)
|
||||
@@ -89,7 +89,7 @@ public class Reachability {
|
||||
// The notification center on which "reachability changed" events are being posted
|
||||
public var notificationCenter: NotificationCenter = NotificationCenter.default
|
||||
|
||||
@available(*, deprecated: 4.0, renamed: "connection.description")
|
||||
@available(*, deprecated, renamed: "connection.description")
|
||||
public var currentReachabilityString: String {
|
||||
return "\(connection)"
|
||||
}
|
||||
@@ -204,43 +204,6 @@ public extension Reachability {
|
||||
SCNetworkReachabilitySetDispatchQueue(reachabilityRef, nil)
|
||||
}
|
||||
|
||||
// MARK: - *** Connection test methods ***
|
||||
@available(*, deprecated: 4.0, message: "Please use `connection != .none`")
|
||||
var isReachable: Bool {
|
||||
guard isReachableFlagSet else { return false }
|
||||
|
||||
if isConnectionRequiredAndTransientFlagSet {
|
||||
return false
|
||||
}
|
||||
|
||||
if isRunningOnDevice {
|
||||
if isOnWWANFlagSet && !reachableOnWWAN {
|
||||
// We don't want to connect when on cellular connection
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@available(*, deprecated: 4.0, message: "Please use `connection == .cellular`")
|
||||
var isReachableViaWWAN: Bool {
|
||||
// Check we're not on the simulator, we're REACHABLE and check we're on WWAN
|
||||
return isRunningOnDevice && isReachableFlagSet && isOnWWANFlagSet
|
||||
}
|
||||
|
||||
@available(*, deprecated: 4.0, message: "Please use `connection == .wifi`")
|
||||
var isReachableViaWiFi: Bool {
|
||||
// Check we're reachable
|
||||
guard isReachableFlagSet else { return false }
|
||||
|
||||
// If reachable we're reachable, but not on an iOS device (i.e. simulator), we must be on WiFi
|
||||
guard isRunningOnDevice else { return true }
|
||||
|
||||
// Check we're NOT on WWAN
|
||||
return !isOnWWANFlagSet
|
||||
}
|
||||
|
||||
var description: String {
|
||||
let W = isRunningOnDevice ? (isOnWWANFlagSet ? "W" : "-") : "X"
|
||||
let R = isReachableFlagSet ? "R" : "-"
|
||||
|
||||
@@ -0,0 +1,330 @@
|
||||
import Foundation
|
||||
|
||||
extension String {
|
||||
|
||||
/// Converts self to an unsigned byte array.
|
||||
public var bytes: [UInt8] {
|
||||
return utf8.map { $0 }
|
||||
}
|
||||
|
||||
/// Converts self to an NSMutableAttributedString.
|
||||
public var attributed: NSMutableAttributedString {
|
||||
return NSMutableAttributedString(string: self)
|
||||
}
|
||||
|
||||
/// Converts self to an NSString.
|
||||
public var ns: NSString {
|
||||
return self as NSString
|
||||
}
|
||||
|
||||
/**
|
||||
Converts string to camel-case.
|
||||
|
||||
Examples:
|
||||
|
||||
```
|
||||
"os version".camelCasedString // "osVersion"
|
||||
"HelloWorld".camelCasedString // "helloWorld"
|
||||
"someword With Characters".camelCasedString // "somewordWithCharacters"
|
||||
```
|
||||
*/
|
||||
public var camelCased: String {
|
||||
guard !isEmpty else { return self }
|
||||
|
||||
if contains(" ") {
|
||||
let first = self[0].lowercased()
|
||||
let cammel = capitalized.replacingOccurrences(of: " ", with: "")
|
||||
let rest = String(cammel.dropFirst())
|
||||
return first + rest
|
||||
} else {
|
||||
let first = self[0].lowercased()
|
||||
let rest = String(dropFirst())
|
||||
return first + rest
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
The base64 encoded version of self.
|
||||
Credit: http://stackoverflow.com/a/29365954
|
||||
*/
|
||||
public var base64Encoded: String? {
|
||||
let utf8str = data(using: .utf8)
|
||||
return utf8str?.base64EncodedString()
|
||||
}
|
||||
|
||||
/**
|
||||
The decoded value of a base64 encoded string
|
||||
Credit: http://stackoverflow.com/a/29365954
|
||||
*/
|
||||
public var base64Decoded: String? {
|
||||
guard let data = Data(base64Encoded: self, options: []) else { return nil }
|
||||
return String(data: data, encoding: .utf8)
|
||||
}
|
||||
|
||||
/**
|
||||
Returns true if every character within the string is a numeric character. Empty strings are
|
||||
considered non-numeric.
|
||||
*/
|
||||
public var isNumeric: Bool {
|
||||
guard !isEmpty else { return false }
|
||||
return trimmingCharacters(in: .decimalDigits).isEmpty
|
||||
}
|
||||
|
||||
/**
|
||||
Replaces all occurences of the pattern on self in-place.
|
||||
|
||||
Examples:
|
||||
|
||||
```
|
||||
"hello".regexInPlace("[aeiou]", "*") // "h*ll*"
|
||||
"hello".regexInPlace("([aeiou])", "<$1>") // "h<e>ll<o>"
|
||||
```
|
||||
*/
|
||||
public mutating func formRegex(_ pattern: String, _ replacement: String) {
|
||||
do {
|
||||
let expression = try NSRegularExpression(pattern: pattern, options: [])
|
||||
let range = NSRange(location: 0, length: count)
|
||||
self = expression.stringByReplacingMatches(in: self, options: [], range: range, withTemplate: replacement)
|
||||
}
|
||||
catch { return }
|
||||
}
|
||||
|
||||
/**
|
||||
Returns a string containing replacements for all pattern matches.
|
||||
|
||||
Examples:
|
||||
|
||||
```
|
||||
"hello".regex("[aeiou]", "*") // "h*ll*"
|
||||
"hello".regex("([aeiou])", "<$1>") // "h<e>ll<o>"
|
||||
```
|
||||
*/
|
||||
public func regex(_ pattern: String, _ replacement: String) -> String {
|
||||
var replacementString = self
|
||||
replacementString.formRegex(pattern, replacement)
|
||||
return replacementString
|
||||
}
|
||||
|
||||
/**
|
||||
Replaces pattern-matched strings, operated upon by a closure, on self in-place.
|
||||
|
||||
- parameter pattern: The pattern to match against.
|
||||
- parameter matches: The closure in which to handle matched strings.
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
"hello".regexInPlace(".") {
|
||||
let s = $0.unicodeScalars
|
||||
let v = s[s.startIndex].value
|
||||
return "\(v) "
|
||||
} // "104 101 108 108 111 "
|
||||
*/
|
||||
public mutating func formRegex(_ pattern: String, _ matches: (String) -> String) {
|
||||
|
||||
let expression: NSRegularExpression
|
||||
do {
|
||||
expression = try NSRegularExpression(pattern: "(\(pattern))", options: [])
|
||||
}
|
||||
catch {
|
||||
print("regex error: \(error)")
|
||||
return
|
||||
}
|
||||
|
||||
let range = NSMakeRange(0, self.count)
|
||||
|
||||
var startOffset = 0
|
||||
|
||||
let results = expression.matches(in: self, options: [], range: range)
|
||||
|
||||
for result in results {
|
||||
|
||||
var endOffset = startOffset
|
||||
|
||||
for i in 1..<result.numberOfRanges {
|
||||
var resultRange = result.range
|
||||
resultRange.location += startOffset
|
||||
|
||||
let startIndex = self.index(self.startIndex, offsetBy: resultRange.location)
|
||||
let endIndex = self.index(self.startIndex, offsetBy: resultRange.location + resultRange.length)
|
||||
let replacementRange = startIndex ..< endIndex
|
||||
|
||||
let match = expression.replacementString(for: result, in: self, offset: startOffset, template: "$\(i)")
|
||||
let replacement = matches(match)
|
||||
|
||||
self.replaceSubrange(replacementRange, with: replacement)
|
||||
|
||||
endOffset += replacement.count - resultRange.length
|
||||
}
|
||||
|
||||
startOffset = endOffset
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Returns a string with pattern-matched strings, operated upon by a closure.
|
||||
|
||||
- parameter pattern: The pattern to match against.
|
||||
- parameter matches: The closure in which to handle matched strings.
|
||||
|
||||
- returns: String containing replacements for the matched pattern.
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
"hello".regex(".") {
|
||||
let s = $0.unicodeScalars
|
||||
let v = s[s.startIndex].value
|
||||
return "\(v) "
|
||||
} // "104 101 108 108 111 "
|
||||
*/
|
||||
public func regex(_ pattern: String, _ matches: (String) -> String) -> String {
|
||||
var replacementString = self
|
||||
replacementString.formRegex(pattern, matches)
|
||||
return replacementString
|
||||
}
|
||||
|
||||
/// Substring at index
|
||||
public subscript(i: Int) -> String {
|
||||
let index = safeIndex(offset: i)
|
||||
let contains = index.flatMap(indices.contains) ?? false
|
||||
|
||||
return contains ? String(self[index!]) : ""
|
||||
}
|
||||
|
||||
/// Substring for range
|
||||
public subscript(r: Range<Int>) -> String {
|
||||
return self[r.lowerBound ... (r.upperBound - 1)]
|
||||
}
|
||||
|
||||
/// Substring for closed range
|
||||
public subscript(r: ClosedRange<Int>) -> String {
|
||||
let startIndex = safeIndex(offset: r.lowerBound)
|
||||
let endIndex = safeIndex(offset: r.upperBound)
|
||||
|
||||
let containsStart = startIndex.flatMap(indices.contains) ?? false
|
||||
let containsEnd = endIndex.flatMap(indices.contains) ?? false
|
||||
|
||||
switch (containsStart, containsEnd) {
|
||||
case (true, true): return String(self[startIndex! ... endIndex!])
|
||||
case (true, false): return String(self[startIndex!...])
|
||||
case (false, true): return String(self[...endIndex!])
|
||||
case (false, false): return ""
|
||||
}
|
||||
}
|
||||
|
||||
/// Substring for countable partial range
|
||||
public subscript(r: CountablePartialRangeFrom<Int>) -> String {
|
||||
let index = safeIndex(offset: r.lowerBound)
|
||||
let contains = index.flatMap(indices.contains) ?? false
|
||||
|
||||
return contains ? String(self[index!...]) : ""
|
||||
}
|
||||
|
||||
/// Substring for partial range through upper bound
|
||||
public subscript(r: PartialRangeThrough<Int>) -> String {
|
||||
let index = safeIndex(offset: r.upperBound)
|
||||
let contains = index.flatMap(indices.contains) ?? false
|
||||
|
||||
return contains ? String(self[...index!]) : ""
|
||||
}
|
||||
|
||||
/// Substring for partial range up to upper bound
|
||||
public subscript(r: PartialRangeUpTo<Int>) -> String {
|
||||
let index = safeIndex(offset: r.upperBound)
|
||||
let contains = index.flatMap(indices.contains) ?? false
|
||||
|
||||
return contains ? String(self[..<index!]) : ""
|
||||
}
|
||||
|
||||
/**
|
||||
Truncates the string to length characters, optionally appending a trailing string. If the string is shorter
|
||||
than the required length, then this function is a non-op.
|
||||
|
||||
- parameter length: The length of string required.
|
||||
- parameter trailing: An optional addition to the end of the string (increasing "length"), such as ellipsis.
|
||||
|
||||
- returns: The truncated string.
|
||||
|
||||
Examples:
|
||||
|
||||
```
|
||||
"hello there".truncated(to: 5) // "hello"
|
||||
"hello there".truncated(to: 5, trailing: "...") // "hello..."
|
||||
```
|
||||
|
||||
*/
|
||||
public func truncated(to length: Int, trailing: String = "") -> String {
|
||||
guard !isEmpty && count > length else { return self }
|
||||
return self[..<length] + trailing
|
||||
}
|
||||
|
||||
public mutating func truncate(to length: Int, trailing: String = "") {
|
||||
self = truncated(to: length, trailing: trailing)
|
||||
}
|
||||
|
||||
/**
|
||||
A bridge for invoking `String.localizedStandardContainsString()`, which is available in iOS 9 and later. If you need to
|
||||
support iOS versions prior to iOS 9, use `compatibleStandardContainsString()` as a means to bridge functionality.
|
||||
If you can support iOS 9 or greater only, use `localizedStandardContainsString()` directly.
|
||||
|
||||
From Apple's Swift 2.1 documentation:
|
||||
|
||||
`localizedStandardContainsString()` is the most appropriate method for doing user-level string searches, similar to how searches are done generally in the system. The search is locale-aware, case and diacritic insensitive. The exact list of search options applied may change over time.
|
||||
|
||||
- parameter string: The string to determine if is contained by self.
|
||||
|
||||
- returns: Returns true if self contains string, taking the current locale into account.
|
||||
*/
|
||||
public func compatibleStandardContains(_ string: String) -> Bool {
|
||||
if #available(iOS 9.0, *) {
|
||||
return localizedStandardContains(string)
|
||||
}
|
||||
return range(of: string, options: [.caseInsensitive, .diacriticInsensitive], locale: .current) != nil
|
||||
}
|
||||
|
||||
/**
|
||||
Convert an NSRange to a Range. There is still a mismatch between the regular expression libraries
|
||||
and NSString/String. This makes it easier to convert between the two. Using this allows complex
|
||||
strings (including emoji, regonial indicattors, etc.) to be manipulated without having to resort
|
||||
to NSString instances.
|
||||
|
||||
Note that it may not always be possible to convert from an NSRange as they are not exactly the same.
|
||||
|
||||
Taken from:
|
||||
http://stackoverflow.com/questions/25138339/nsrange-to-rangestring-index
|
||||
|
||||
- parameter nsRange: The NSRange instance to covert to a Range.
|
||||
|
||||
- returns: The Range, if it was possible to convert. Otherwise nil.
|
||||
*/
|
||||
public func range(from nsRange: NSRange) -> Range<String.Index>? {
|
||||
return Range(nsRange, in: self)
|
||||
}
|
||||
|
||||
/**
|
||||
Convert a Range to an NSRange. There is still a mismatch between the regular expression libraries
|
||||
and NSString/String. This makes it easier to convert between the two. Using this allows complex
|
||||
strings (including emoji, regonial indicators, etc.) to be manipulated without having to resort
|
||||
to NSString instances.
|
||||
|
||||
Taken from:
|
||||
http://stackoverflow.com/questions/25138339/nsrange-to-rangestring-index
|
||||
|
||||
- parameter range: The Range instance to conver to an NSRange.
|
||||
|
||||
- returns: The NSRange converted from the input. This will always succeed.
|
||||
*/
|
||||
public func nsRange(from range: Range<String.Index>) -> NSRange {
|
||||
return NSRange(range, in: self)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private extension String {
|
||||
|
||||
func safeIndex(offset: Int) -> String.Index? {
|
||||
return index(startIndex, offsetBy: offset.limited(0, .max), limitedBy: endIndex)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,460 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
extension UIColor {
|
||||
|
||||
/**
|
||||
Returns a UIColor from the given hexidecimal integer.
|
||||
- parameter hex: The hex component of the color object, specified as a value from 0x000000 to 0xFFFFFF.
|
||||
- parameter alpha: The opacity component of the color object, specified as a value from 0.0 to 1.0 (optional).
|
||||
- returns: A UIColor initialized with the given color value.
|
||||
*/
|
||||
public convenience init(hex: UInt32, alpha: CGFloat = 1) {
|
||||
assert((0...0xFFFFFF).contains(hex), "hex must be a value between 0x000000 and 0xFFFFFF")
|
||||
assert((0...1).contains(alpha), "alpha must be a value between 0.0 and 1.0")
|
||||
|
||||
let (r, g, b) = Model.hex(hex).rgb
|
||||
self.init(red: r/255, green: g/255, blue: b/255, alpha: alpha)
|
||||
}
|
||||
|
||||
/**
|
||||
Returns a UIColor from a hue-saturation-lightness (HSL) set.
|
||||
- parameter hue: The hue component of the color object, specified as a value from 0.0 to 1.0.
|
||||
- parameter saturation: The saturation component of the color object, specified as a value from 0.0 to 1.0.
|
||||
- parameter lightness: The lightness component of the color object, specified as a value from 0.0 to 1.0.
|
||||
- parameter alpha: The opacity component of the color object, specified as a value from 0.0 to 1.0 (optional).
|
||||
- returns: A UIColor initialized with the given color value.
|
||||
*/
|
||||
public convenience init(hue: CGFloat, saturation: CGFloat, lightness: CGFloat, alpha: CGFloat = 1) {
|
||||
assert((0...1).contains(hue), "hue value must be a value between 0.0 and 1.0")
|
||||
assert((0...1).contains(saturation), "saturation must be a value between 0.0 and 1.0")
|
||||
assert((0...1).contains(lightness), "lightness must be a value between 0.0 and 1.0")
|
||||
assert((0...1).contains(alpha), "alpha must be a value between 0.0 and 1.0")
|
||||
|
||||
let (r, g, b) = Model.hsl(hue, saturation, lightness).rgb
|
||||
self.init(red: r/255, green: g/255, blue: b/255, alpha: alpha)
|
||||
}
|
||||
|
||||
/**
|
||||
Returns a UIColor from a cyan-magenta-yellow-key (CMYK) set.
|
||||
- parameter cyan: The cyan component of the color object, specified as a value from 0.0 to 1.0.
|
||||
- parameter magenta: The magenta component of the color object, specified as a value from 0.0 to 1.0.
|
||||
- parameter yellow: The yellow component of the color object, specified as a value from 0.0 to 1.0.
|
||||
- parameter key: The key (black) component of the color object, specified as a value from 0.0 to 1.0.
|
||||
- parameter alpha: The opacity component of the color object, specified as a value from 0.0 to 1.0 (optional).
|
||||
- returns: A UIColor initialized with the given color value.
|
||||
*/
|
||||
public convenience init(cyan: CGFloat, magenta: CGFloat, yellow: CGFloat, key: CGFloat, alpha: CGFloat = 1) {
|
||||
assert((0...1).contains(cyan), "cyan value must be a value between 0.0 and 1.0")
|
||||
assert((0...1).contains(magenta), "magenta must be a value between 0.0 and 1.0")
|
||||
assert((0...1).contains(yellow), "yellow must be a value between 0.0 and 1.0")
|
||||
assert((0...1).contains(key), "key must be a value between 0.0 and 1.0")
|
||||
assert((0...1).contains(alpha), "alpha must be a value between 0.0 and 1.0")
|
||||
|
||||
let (r, g, b) = Model.cmyk(cyan, magenta, yellow, key).rgb
|
||||
self.init(red: r/255, green: g/255, blue: b/255, alpha: alpha)
|
||||
}
|
||||
|
||||
/**
|
||||
Returns a UIColor from a given hex color string.
|
||||
- parameter hexString: The hex color string, e.g.: "#9443FB" or "9443FB".
|
||||
- returns: A UIColor initialized with the color specified by the hexString.
|
||||
*/
|
||||
public convenience init(hexString: String, alpha: CGFloat = 1) {
|
||||
var hexString = hexString
|
||||
if hexString.hasPrefix("#") {
|
||||
hexString = hexString[1...]
|
||||
}
|
||||
let scanner = Scanner(string: hexString)
|
||||
var hexEquivalent: UInt32 = 0
|
||||
|
||||
if !scanner.scanHexInt32(&hexEquivalent) {
|
||||
assertionFailure("hexString did not contain a valid hex value")
|
||||
}
|
||||
|
||||
self.init(hex: hexEquivalent, alpha: alpha)
|
||||
}
|
||||
|
||||
/**
|
||||
Returns a UIColor initialized with color components divided by 255.0.
|
||||
- parameter red: Integer representation of the red component in range of 0-255.
|
||||
- parameter green: Integer representation of the green component in range of 0-255.
|
||||
- parameter blue: Integer representation of the blue component in range of 0-255.
|
||||
*/
|
||||
public convenience init(red: UInt8, green: UInt8, blue: UInt8) {
|
||||
self.init(red: CGFloat(red)/255.0, green: CGFloat(green)/255.0, blue: CGFloat(blue)/255.0, alpha: 1.0)
|
||||
}
|
||||
|
||||
/// Returns a random UIColor with hue, saturation, and brightness ranging from 0.5 to 1.0.
|
||||
public static var random: UIColor {
|
||||
let component = { CGFloat(arc4random() % 128)/256.0 + 0.5 }
|
||||
return UIColor(hue: component(), saturation: component(), brightness: component(), alpha: 1)
|
||||
}
|
||||
|
||||
/**
|
||||
Lightens the given color by the given percentage.
|
||||
- parameter amount: The percentage by which to lighten the color. Valid values are from `0.0` to `1.0`, or for a more readable format `0%` to `100%`.
|
||||
- returns: The lightened color.
|
||||
*/
|
||||
public final func lightened(by amount: CGFloat) -> UIColor {
|
||||
assert((0...1).contains(amount), "amount must be in range 0-100%")
|
||||
|
||||
let (h, s, l) = hsl
|
||||
return UIColor(hue: h, saturation: s, lightness: l * (1 + amount), alpha: rgba.a)
|
||||
}
|
||||
|
||||
/**
|
||||
Darkens the given color by the given percentage.
|
||||
- parameter amount: The percentage by which to darken the color. Valid values are from `0.0` to `1.0`, or for a more readable format `0%` to `100%`.
|
||||
- returns: The darkened color.
|
||||
*/
|
||||
public final func darkened(by amount: CGFloat) -> UIColor {
|
||||
assert((0...1).contains(amount), "amount must be in range 0-100%")
|
||||
|
||||
let (h, s, l) = hsl
|
||||
return UIColor(hue: h, saturation: s, lightness: l * (1 - amount), alpha: rgba.a)
|
||||
}
|
||||
|
||||
/**
|
||||
Returns the color represenation as a hexadecimal string, prefixed with '#'.
|
||||
- returns: The hexadecimal string representation of the color.
|
||||
*/
|
||||
public final var hexString: String {
|
||||
return String(format:"#%06x", hex)
|
||||
}
|
||||
|
||||
/**
|
||||
Returns the color representation as a 32-bit integer.
|
||||
- returns: A UInt32 that represents the hexadecimal color.
|
||||
*/
|
||||
public final var hex: UInt32 {
|
||||
let (r, g, b, _) = self.rgba
|
||||
return Model.rgb(r * 255, g * 255, b * 255).hex
|
||||
}
|
||||
|
||||
/**
|
||||
Returns the RGBA (red, green, blue, alpha) components, specified as values from 0.0 to 1.0.
|
||||
- returns: The RGBA components as a tuple (r, g, b, a).
|
||||
*/
|
||||
public final var rgba: (r: CGFloat, g: CGFloat, b: CGFloat, a: CGFloat) {
|
||||
var (r, g, b, a): (CGFloat, CGFloat, CGFloat, CGFloat) = (0, 0, 0, 0)
|
||||
getRed(&r, green: &g, blue: &b, alpha: &a)
|
||||
|
||||
return (r, g, b, a)
|
||||
}
|
||||
|
||||
/**
|
||||
Returns the HSL (hue, saturation, lightness) components, specified as values from 0.0 to 1.0.
|
||||
- returns: The HSL components as a tuple (h, s, l).
|
||||
*/
|
||||
public final var hsl: (h: CGFloat, s: CGFloat, l: CGFloat) {
|
||||
let (r, g, b, _) = rgba
|
||||
return Model.rgb(r * 255, g * 255, b * 255).hsl
|
||||
}
|
||||
|
||||
/**
|
||||
Returns the HSB (hue, saturation, brightness) components, specified as values from 0.0 to 1.0.
|
||||
- returns: The HSB components as a tuple (h, s, b).
|
||||
*/
|
||||
public final var hsb: (h: CGFloat, s: CGFloat, b: CGFloat) {
|
||||
let (r, g, b, _) = rgba
|
||||
return Model.rgb(r * 255, g * 255, b * 255).hsb
|
||||
}
|
||||
|
||||
/**
|
||||
Returns the CMYK (cyan, magenta, yellow, key) components, specified as values from 0.0 to 1.0.
|
||||
- returns: The CMYK components as a tuple (c, m, y, k).
|
||||
*/
|
||||
public final var cmyk: (c: CGFloat, m: CGFloat, y: CGFloat, k: CGFloat) {
|
||||
let (r, g, b, _) = rgba
|
||||
return Model.rgb(r * 255, g * 255, b * 255).cmyk
|
||||
}
|
||||
|
||||
/**
|
||||
Returns an alpha-adjusted UIColor.
|
||||
- returns: A UIColor with an adjust alpha component (shorthand for `colorWithAlphaComponent`).
|
||||
*/
|
||||
public final func alpha(_ alpha: CGFloat) -> UIColor {
|
||||
return withAlphaComponent(alpha)
|
||||
}
|
||||
|
||||
/**
|
||||
Returns a UIColor from the given hexidecimal integer.
|
||||
- parameter hex: The color value.
|
||||
- parameter alpha: The alpha component.
|
||||
- returns: A UIColor initialized with the given hex value.
|
||||
*/
|
||||
public static func hex(_ hex: UInt32, alpha: CGFloat = 1) -> UIColor {
|
||||
return UIColor(hex: hex, alpha: alpha)
|
||||
}
|
||||
|
||||
/**
|
||||
Returns a UIColor from the given RGB components.
|
||||
- parameter red: The red component in range of 0-255.
|
||||
- parameter green: The green component in range of 0-255.
|
||||
- parameter blue: The blue component in range of 0-255.
|
||||
- parameter alpha: The alpha component.
|
||||
- returns: A UIColor initialized with the given RGB components.
|
||||
*/
|
||||
public static func rgb(_ red: UInt8, _ green: UInt8, _ blue: UInt8, alpha: CGFloat = 1) -> UIColor {
|
||||
return UIColor(red: red, green: green, blue: blue).alpha(alpha)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Model
|
||||
|
||||
extension UIColor {
|
||||
|
||||
/**
|
||||
Model is an enum for describing and converting color models.
|
||||
|
||||
- `rgb`: Red, Green, Blue color representation
|
||||
- `hsl`: Hue, Saturation, Lightness color representation
|
||||
- `hsb`: Hue, Saturation, Brightness color representation
|
||||
- `cmyk`: Cyan, Magenta, Yellow, Key (Black) color representation
|
||||
- `hex`: UInt32 (hex) color representation
|
||||
*/
|
||||
public enum Model {
|
||||
/// Red, Green, Blue
|
||||
case rgb(CGFloat, CGFloat, CGFloat)
|
||||
/// Hue, Saturation, Lightness
|
||||
case hsl(CGFloat, CGFloat, CGFloat)
|
||||
/// Hue, Saturation, Brightness
|
||||
case hsb(CGFloat, CGFloat, CGFloat)
|
||||
/// Cyan, Magenta, Yellow, Key (Black)
|
||||
case cmyk(CGFloat, CGFloat, CGFloat, CGFloat)
|
||||
/// UInt32 (hex)
|
||||
case hex(UInt32)
|
||||
|
||||
/// Returns the model as an RGB tuple
|
||||
public var rgb: (r: CGFloat, g: CGFloat, b: CGFloat) {
|
||||
switch self {
|
||||
case .rgb(let rgb):
|
||||
return rgb
|
||||
case .hsl(let h, let s, let l):
|
||||
return convert(hsl: h, s, l)
|
||||
case .hsb(let h, let s, let b):
|
||||
return convert(hsb: h, s, b)
|
||||
case .cmyk(let c, let m, let y, let k):
|
||||
return convert(cmyk: c, m, y, k)
|
||||
case .hex(let hex):
|
||||
return convert(hex: hex)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the model as an HSL tuple
|
||||
public var hsl: (h: CGFloat, s: CGFloat, l: CGFloat) {
|
||||
switch self {
|
||||
case .rgb(let r, let g, let b):
|
||||
return convert(rgb: r, g, b)
|
||||
case .hsl(let hsl):
|
||||
return hsl
|
||||
case .hsb, .cmyk, .hex:
|
||||
let (r, g, b) = self.rgb
|
||||
return convert(rgb: r, g, b)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the model as an HSB tuple
|
||||
public var hsb: (h: CGFloat, s: CGFloat, b: CGFloat) {
|
||||
switch self {
|
||||
case .rgb(let r, let g, let b):
|
||||
return convert(rgb: r, g, b)
|
||||
case .hsl, .cmyk, .hex:
|
||||
let (r, g, b) = self.rgb
|
||||
return convert(rgb: r, g, b)
|
||||
case .hsb(let hsb):
|
||||
return hsb
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the model as a CMYK tuple
|
||||
public var cmyk: (c: CGFloat, m: CGFloat, y: CGFloat, k: CGFloat) {
|
||||
switch self {
|
||||
case .rgb(let r, let g, let b):
|
||||
return convert(rgb: r, g, b)
|
||||
case .hsl, .hsb, .hex:
|
||||
let (r, g, b) = self.rgb
|
||||
return convert(rgb: r, g, b)
|
||||
case .cmyk(let cmyk):
|
||||
return cmyk
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the model as a UInt32 (hex) value
|
||||
public var hex: UInt32 {
|
||||
switch self {
|
||||
case .rgb(let r, let g, let b):
|
||||
return convert(rgb: r, g, b)
|
||||
case .hsl, .hsb, .cmyk:
|
||||
let (r, g, b) = self.rgb
|
||||
return convert(rgb: r, g, b)
|
||||
case .hex(let hex):
|
||||
return hex
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private color model conversions
|
||||
|
||||
/// Converts RGB to HSL (https://en.wikipedia.org/wiki/HSL_and_HSV)
|
||||
private func convert(rgb r: CGFloat, _ g: CGFloat, _ b: CGFloat) -> (h: CGFloat, s: CGFloat, l: CGFloat) {
|
||||
|
||||
let r = r / 255
|
||||
let g = g / 255
|
||||
let b = b / 255
|
||||
|
||||
let max = Swift.max(r, g, b)
|
||||
let min = Swift.min(r, g, b)
|
||||
|
||||
var h, s: CGFloat
|
||||
let l = (max + min) / 2
|
||||
|
||||
if max == min {
|
||||
h = 0
|
||||
s = 0
|
||||
}
|
||||
else {
|
||||
let d = max - min
|
||||
s = (l > 0.5) ? d / (2 - max - min) : d / (max + min)
|
||||
|
||||
switch max {
|
||||
case r: h = (g - b) / d + (g < b ? 6 : 0)
|
||||
case g: h = (b - r) / d + 2
|
||||
case b: h = (r - g) / d + 4
|
||||
default: h = 0
|
||||
}
|
||||
|
||||
h /= 6;
|
||||
}
|
||||
|
||||
return (h, s, l)
|
||||
}
|
||||
|
||||
/// Converts HSL to RGB (https://en.wikipedia.org/wiki/HSL_and_HSV)
|
||||
private func convert(hsl h: CGFloat, _ s: CGFloat, _ l: CGFloat) -> (r: CGFloat, g: CGFloat, b: CGFloat) {
|
||||
|
||||
let r, g, b: CGFloat
|
||||
|
||||
if s == 0 {
|
||||
r = l
|
||||
g = l
|
||||
b = l
|
||||
}
|
||||
else {
|
||||
let c = (1 - abs(2 * l - 1)) * s
|
||||
let x = c * (1 - abs((h * 6).truncatingRemainder(dividingBy: 2) - 1))
|
||||
let m = l - c/2
|
||||
|
||||
switch h * 6 {
|
||||
case 0..<1: (r, g, b) = (c, x, 0) + m
|
||||
case 1..<2: (r, g, b) = (x, c, 0) + m
|
||||
case 2..<3: (r, g, b) = (0, c, x) + m
|
||||
case 3..<4: (r, g, b) = (0, x, c) + m
|
||||
case 4..<5: (r, g, b) = (x, 0, c) + m
|
||||
case 5..<6: (r, g, b) = (c, 0, x) + m
|
||||
default: (r, g, b) = (0, 0, 0) + m
|
||||
}
|
||||
}
|
||||
|
||||
return (round(r * 255), round(g * 255), round(b * 255))
|
||||
}
|
||||
|
||||
/// Converts RGB to HSB (https://en.wikipedia.org/wiki/HSL_and_HSV)
|
||||
private func convert(rgb r: CGFloat, _ g: CGFloat, _ b: CGFloat) -> (h: CGFloat, s: CGFloat, b: CGFloat) {
|
||||
var h, s, v: CGFloat
|
||||
|
||||
let r = r / 255
|
||||
let g = g / 255
|
||||
let b = b / 255
|
||||
|
||||
let max = Swift.max(r, g, b)
|
||||
let min = Swift.min(r, g, b)
|
||||
let d = max - min
|
||||
|
||||
if d == 0 {
|
||||
h = 0
|
||||
s = 0
|
||||
}
|
||||
else {
|
||||
s = (max == 0) ? 0 : d / max
|
||||
|
||||
switch max {
|
||||
case r: h = ((g - b) / d) + (g < b ? 6 : 0)
|
||||
case g: h = ((b - r) / d) + 2
|
||||
case b: h = ((r - g) / d) + 4
|
||||
default: h = 0
|
||||
}
|
||||
|
||||
h /= 6;
|
||||
}
|
||||
|
||||
v = max
|
||||
|
||||
return (h, s, v)
|
||||
}
|
||||
|
||||
/// Converts HSB to RGB (https://en.wikipedia.org/wiki/HSL_and_HSV)
|
||||
private func convert(hsb h: CGFloat, _ s: CGFloat, _ b: CGFloat) -> (r: CGFloat, g: CGFloat, b: CGFloat) {
|
||||
|
||||
let c = b * s
|
||||
let x = c * (1 - abs((h * 6).truncatingRemainder(dividingBy: 2) - 1))
|
||||
let m = b - c
|
||||
|
||||
var r, g, b: CGFloat
|
||||
|
||||
switch h * 6 {
|
||||
case 0..<1: (r, g, b) = (c, x, 0) + m
|
||||
case 1..<2: (r, g, b) = (x, c, 0) + m
|
||||
case 2..<3: (r, g, b) = (0, c, x) + m
|
||||
case 3..<4: (r, g, b) = (0, x, c) + m
|
||||
case 4..<5: (r, g, b) = (x, 0, c) + m
|
||||
case 5..<6: (r, g, b) = (c, 0, x) + m
|
||||
default: (r, g, b) = (0, 0, 0) + m
|
||||
}
|
||||
|
||||
return (round(r * 255), round(g * 255), round(b * 255))
|
||||
}
|
||||
|
||||
/// Converts UInt32 to RGB
|
||||
private func convert(hex: UInt32) -> (r: CGFloat, g: CGFloat, b: CGFloat) {
|
||||
let r = CGFloat((hex >> 16) & 0xFF)
|
||||
let g = CGFloat((hex >> 8) & 0xFF)
|
||||
let b = CGFloat(hex & 0xFF)
|
||||
|
||||
return (r, g, b)
|
||||
}
|
||||
|
||||
/// Converts RGB to UInt32
|
||||
private func convert(rgb r: CGFloat, _ g: CGFloat, _ b: CGFloat) -> UInt32 {
|
||||
return (UInt32(r) << 16) | (UInt32(g) << 8) | UInt32(b)
|
||||
}
|
||||
|
||||
/// Converts RGB to CMYK (http://www.rapidtables.com/convert/color/rgb-to-cmyk.htm)
|
||||
private func convert(rgb r: CGFloat, _ g: CGFloat, _ b: CGFloat) -> (c: CGFloat, m: CGFloat, y: CGFloat, k: CGFloat) {
|
||||
let r = r / 255
|
||||
let g = g / 255
|
||||
let b = b / 255
|
||||
|
||||
let k = 1 - max(r, g, b)
|
||||
let c = (k == 1) ? 0 : (1 - r - k) / (1 - k)
|
||||
let m = (k == 1) ? 0 : (1 - g - k) / (1 - k)
|
||||
let y = (k == 1) ? 0 : (1 - b - k) / (1 - k)
|
||||
|
||||
return (c, m, y, k)
|
||||
}
|
||||
|
||||
/// Converts CMYK to RGB (http://www.rapidtables.com/convert/color/cmyk-to-rgb.htm)
|
||||
private func convert(cmyk c: CGFloat, _ m: CGFloat, _ y: CGFloat, _ k: CGFloat) -> (r: CGFloat, g: CGFloat, b: CGFloat) {
|
||||
let r = 255 * (1 - c) * (1 - k)
|
||||
let g = 255 * (1 - m) * (1 - k)
|
||||
let b = 255 * (1 - y) * (1 - k)
|
||||
|
||||
return (round(r), round(g), round(b))
|
||||
}
|
||||
|
||||
/// Private operator for HSL and HSB conversion
|
||||
private func +(lhs: (CGFloat, CGFloat, CGFloat), rhs: CGFloat) -> (CGFloat, CGFloat, CGFloat) {
|
||||
return (lhs.0 + rhs, lhs.1 + rhs, lhs.2 + rhs)
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
extension UIColor {
|
||||
|
||||
/// Returns a random UIColor
|
||||
public static var random: UIColor {
|
||||
let hue : CGFloat = CGFloat(arc4random() % 256) / 256 // use 256 to get full range from 0.0 to 1.0
|
||||
let saturation : CGFloat = CGFloat(arc4random() % 128) / 256 + 0.5 // from 0.5 to 1.0 to stay away from white
|
||||
let brightness : CGFloat = CGFloat(arc4random() % 128) / 256 + 0.5 // from 0.5 to 1.0 to stay away from black
|
||||
|
||||
return UIColor(hue: hue, saturation: saturation, brightness: brightness, alpha: 1)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user