Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9b222b328e | |||
| 64d0f96f83 | |||
| 7556ac505e | |||
| 96abfee256 | |||
| 86a06411d7 | |||
| 8031f5a28f | |||
| fdd2ed5b7c | |||
| 5cabfab574 | |||
| 535b76fea0 | |||
| 5b0672ee60 | |||
| 68eee74e76 | |||
| 5a4a0f30c4 | |||
| cc2a7348fd | |||
| d89555b9ea | |||
| dc98e270f0 | |||
| e18a72804d | |||
| aa9c4e22a6 | |||
| 1b7f547eda | |||
| dac53d1ac5 | |||
| 5af96519f7 |
@@ -0,0 +1 @@
|
||||
4.2
|
||||
@@ -1,5 +1,5 @@
|
||||
PODS:
|
||||
- utopia (0.1.0)
|
||||
- utopia (0.2.1)
|
||||
|
||||
DEPENDENCIES:
|
||||
- utopia (from `../`)
|
||||
@@ -9,8 +9,8 @@ EXTERNAL SOURCES:
|
||||
:path: "../"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
utopia: 62f8a2a9add7c9b605c03e8c20972831a569a084
|
||||
utopia: 5f8b06c09df6ecc26f8f64a01bce85b887cfb4ab
|
||||
|
||||
PODFILE CHECKSUM: 632947ed90777377758aa9384f691613d327ceb4
|
||||
|
||||
COCOAPODS: 1.5.3
|
||||
COCOAPODS: 1.8.4
|
||||
|
||||
+4
-4
@@ -1,8 +1,7 @@
|
||||
{
|
||||
"name": "utopia",
|
||||
"version": "0.1.0",
|
||||
"summary": "A short description of utopia.",
|
||||
"description": "TODO: Add long description of the pod here.",
|
||||
"version": "0.2.1",
|
||||
"summary": "Common utilities for Swift projects",
|
||||
"homepage": "https://github.com/Ramotion/utopia",
|
||||
"license": {
|
||||
"type": "MIT",
|
||||
@@ -10,10 +9,11 @@
|
||||
},
|
||||
"authors": {
|
||||
"Dmitriy Kalachev": "dima.k@ramotion.com"
|
||||
"Ramotion": "igor.k@ramotion.com"
|
||||
},
|
||||
"source": {
|
||||
"git": "https://github.com/Ramotion/utopia.git",
|
||||
"tag": "0.1.0"
|
||||
"tag": "0.2.1"
|
||||
},
|
||||
"platforms": {
|
||||
"ios": "10.0"
|
||||
|
||||
Generated
+3
-3
@@ -1,5 +1,5 @@
|
||||
PODS:
|
||||
- utopia (0.1.0)
|
||||
- utopia (0.2.1)
|
||||
|
||||
DEPENDENCIES:
|
||||
- utopia (from `../`)
|
||||
@@ -9,8 +9,8 @@ EXTERNAL SOURCES:
|
||||
:path: "../"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
utopia: 62f8a2a9add7c9b605c03e8c20972831a569a084
|
||||
utopia: 5f8b06c09df6ecc26f8f64a01bce85b887cfb4ab
|
||||
|
||||
PODFILE CHECKSUM: 632947ed90777377758aa9384f691613d327ceb4
|
||||
|
||||
COCOAPODS: 1.5.3
|
||||
COCOAPODS: 1.8.4
|
||||
|
||||
+736
-855
File diff suppressed because it is too large
Load Diff
+26
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>${EXECUTABLE_NAME}</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>${PRODUCT_BUNDLE_IDENTIFIER}</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>${PRODUCT_NAME}</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>${CURRENT_PROJECT_VERSION}</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string></string>
|
||||
</dict>
|
||||
</plist>
|
||||
+28
-10
@@ -3,10 +3,15 @@ set -e
|
||||
set -u
|
||||
set -o pipefail
|
||||
|
||||
function on_error {
|
||||
echo "$(realpath -mq "${0}"):$1: error: Unexpected failure"
|
||||
}
|
||||
trap 'on_error $LINENO' ERR
|
||||
|
||||
if [ -z ${FRAMEWORKS_FOLDER_PATH+x} ]; then
|
||||
# If FRAMEWORKS_FOLDER_PATH is not set, then there's nowhere for us to copy
|
||||
# frameworks to, so exit 0 (signalling the script phase was successful).
|
||||
exit 0
|
||||
# If FRAMEWORKS_FOLDER_PATH is not set, then there's nowhere for us to copy
|
||||
# frameworks to, so exit 0 (signalling the script phase was successful).
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "mkdir -p ${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}"
|
||||
@@ -36,8 +41,8 @@ install_framework()
|
||||
local destination="${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}"
|
||||
|
||||
if [ -L "${source}" ]; then
|
||||
echo "Symlinked..."
|
||||
source="$(readlink "${source}")"
|
||||
echo "Symlinked..."
|
||||
source="$(readlink "${source}")"
|
||||
fi
|
||||
|
||||
# Use filter instead of exclude so missing patterns don't throw errors.
|
||||
@@ -47,8 +52,13 @@ install_framework()
|
||||
local basename
|
||||
basename="$(basename -s .framework "$1")"
|
||||
binary="${destination}/${basename}.framework/${basename}"
|
||||
|
||||
if ! [ -r "$binary" ]; then
|
||||
binary="${destination}/${basename}"
|
||||
elif [ -L "${binary}" ]; then
|
||||
echo "Destination binary is symlinked..."
|
||||
dirname="$(dirname "${binary}")"
|
||||
binary="${dirname}/$(readlink "${binary}")"
|
||||
fi
|
||||
|
||||
# Strip invalid architectures so "fat" simulator / device frameworks work on device
|
||||
@@ -62,7 +72,7 @@ install_framework()
|
||||
# Embed linked Swift runtime libraries. No longer necessary as of Xcode 7.
|
||||
if [ "${XCODE_VERSION_MAJOR}" -lt 7 ]; then
|
||||
local swift_runtime_libs
|
||||
swift_runtime_libs=$(xcrun otool -LX "$binary" | grep --color=never @rpath/libswift | sed -E s/@rpath\\/\(.+dylib\).*/\\1/g | uniq -u && exit ${PIPESTATUS[0]})
|
||||
swift_runtime_libs=$(xcrun otool -LX "$binary" | grep --color=never @rpath/libswift | sed -E s/@rpath\\/\(.+dylib\).*/\\1/g | uniq -u)
|
||||
for lib in $swift_runtime_libs; do
|
||||
echo "rsync -auv \"${SWIFT_STDLIB_PATH}/${lib}\" \"${destination}\""
|
||||
rsync -auv "${SWIFT_STDLIB_PATH}/${lib}" "${destination}"
|
||||
@@ -84,7 +94,7 @@ install_dsym() {
|
||||
binary="${DERIVED_FILES_DIR}/${basename}.framework.dSYM/Contents/Resources/DWARF/${basename}"
|
||||
|
||||
# Strip invalid architectures so "fat" simulator / device frameworks work on device
|
||||
if [[ "$(file "$binary")" == *"Mach-O dSYM companion"* ]]; then
|
||||
if [[ "$(file "$binary")" == *"Mach-O "*"dSYM companion"* ]]; then
|
||||
strip_invalid_archs "$binary"
|
||||
fi
|
||||
|
||||
@@ -99,10 +109,18 @@ install_dsym() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Copies the bcsymbolmap files of a vendored framework
|
||||
install_bcsymbolmap() {
|
||||
local bcsymbolmap_path="$1"
|
||||
local destination="${BUILT_PRODUCTS_DIR}"
|
||||
echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${bcsymbolmap_path}" "${destination}""
|
||||
rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${bcsymbolmap_path}" "${destination}"
|
||||
}
|
||||
|
||||
# Signs a framework with the provided identity
|
||||
code_sign_if_enabled() {
|
||||
if [ -n "${EXPANDED_CODE_SIGN_IDENTITY}" -a "${CODE_SIGNING_REQUIRED:-}" != "NO" -a "${CODE_SIGNING_ALLOWED}" != "NO" ]; then
|
||||
# Use the current code_sign_identitiy
|
||||
if [ -n "${EXPANDED_CODE_SIGN_IDENTITY:-}" -a "${CODE_SIGNING_REQUIRED:-}" != "NO" -a "${CODE_SIGNING_ALLOWED}" != "NO" ]; then
|
||||
# Use the current code_sign_identity
|
||||
echo "Code Signing $1 with Identity ${EXPANDED_CODE_SIGN_IDENTITY_NAME}"
|
||||
local code_sign_cmd="/usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} ${OTHER_CODE_SIGN_FLAGS:-} --preserve-metadata=identifier,entitlements '$1'"
|
||||
|
||||
@@ -131,7 +149,7 @@ strip_invalid_archs() {
|
||||
for arch in $binary_archs; do
|
||||
if ! [[ "${ARCHS}" == *"$arch"* ]]; then
|
||||
# Strip non-valid architectures in-place
|
||||
lipo -remove "$arch" -output "$binary" "$binary" || exit 1
|
||||
lipo -remove "$arch" -output "$binary" "$binary"
|
||||
stripped="$stripped $arch"
|
||||
fi
|
||||
done
|
||||
|
||||
+3
-2
@@ -1,11 +1,12 @@
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES
|
||||
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/utopia"
|
||||
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
||||
HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/utopia/utopia.framework/Headers"
|
||||
LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks'
|
||||
OTHER_CFLAGS = $(inherited) -iquote "${PODS_CONFIGURATION_BUILD_DIR}/utopia/utopia.framework/Headers"
|
||||
OTHER_LDFLAGS = $(inherited) -framework "utopia"
|
||||
OTHER_SWIFT_FLAGS = $(inherited) "-D" "COCOAPODS"
|
||||
OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS
|
||||
PODS_BUILD_DIR = ${BUILD_DIR}
|
||||
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
||||
PODS_PODFILE_DIR_PATH = ${SRCROOT}/.
|
||||
PODS_ROOT = ${SRCROOT}/Pods
|
||||
USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES
|
||||
|
||||
Generated
+3
-2
@@ -1,11 +1,12 @@
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES
|
||||
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/utopia"
|
||||
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
||||
HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/utopia/utopia.framework/Headers"
|
||||
LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks'
|
||||
OTHER_CFLAGS = $(inherited) -iquote "${PODS_CONFIGURATION_BUILD_DIR}/utopia/utopia.framework/Headers"
|
||||
OTHER_LDFLAGS = $(inherited) -framework "utopia"
|
||||
OTHER_SWIFT_FLAGS = $(inherited) "-D" "COCOAPODS"
|
||||
OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS
|
||||
PODS_BUILD_DIR = ${BUILD_DIR}
|
||||
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
||||
PODS_PODFILE_DIR_PATH = ${SRCROOT}/.
|
||||
PODS_ROOT = ${SRCROOT}/Pods
|
||||
USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>${EXECUTABLE_NAME}</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>${PRODUCT_BUNDLE_IDENTIFIER}</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>${PRODUCT_NAME}</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.2.1</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>${CURRENT_PROJECT_VERSION}</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string></string>
|
||||
</dict>
|
||||
</plist>
|
||||
+2
-1
@@ -1,9 +1,10 @@
|
||||
CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/utopia
|
||||
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
||||
OTHER_SWIFT_FLAGS = $(inherited) "-D" "COCOAPODS"
|
||||
OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS
|
||||
PODS_BUILD_DIR = ${BUILD_DIR}
|
||||
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
||||
PODS_ROOT = ${SRCROOT}
|
||||
PODS_TARGET_SRCROOT = ${PODS_ROOT}/../..
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier}
|
||||
SKIP_INSTALL = YES
|
||||
USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES
|
||||
|
||||
@@ -8,8 +8,6 @@
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
607FACD61AFB9204008FA782 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACD51AFB9204008FA782 /* AppDelegate.swift */; };
|
||||
607FACD81AFB9204008FA782 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACD71AFB9204008FA782 /* ViewController.swift */; };
|
||||
607FACDB1AFB9204008FA782 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 607FACD91AFB9204008FA782 /* Main.storyboard */; };
|
||||
607FACDD1AFB9204008FA782 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 607FACDC1AFB9204008FA782 /* Images.xcassets */; };
|
||||
607FACE01AFB9204008FA782 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 607FACDE1AFB9204008FA782 /* LaunchScreen.xib */; };
|
||||
CEAFA6DEE8FD46FA928FC6B9 /* Pods_utopia_Example.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0D96C1039201BCADF7E730F6 /* Pods_utopia_Example.framework */; };
|
||||
@@ -18,15 +16,11 @@
|
||||
/* Begin PBXFileReference section */
|
||||
0D96C1039201BCADF7E730F6 /* Pods_utopia_Example.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_utopia_Example.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
12909272F90F0829F3E16C48 /* utopia.podspec */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = utopia.podspec; path = ../utopia.podspec; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.ruby; };
|
||||
3D6E6CE2CA9E2A4ED29C9C80 /* Pods-utopia_Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-utopia_Tests.release.xcconfig"; path = "Pods/Target Support Files/Pods-utopia_Tests/Pods-utopia_Tests.release.xcconfig"; sourceTree = "<group>"; };
|
||||
607FACD01AFB9204008FA782 /* utopia_Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = utopia_Example.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
607FACD41AFB9204008FA782 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
607FACD51AFB9204008FA782 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
607FACD71AFB9204008FA782 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
|
||||
607FACDA1AFB9204008FA782 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||
607FACDC1AFB9204008FA782 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = "<group>"; };
|
||||
607FACDF1AFB9204008FA782 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = "<group>"; };
|
||||
6F466667E3C01068D5F3BABF /* Pods-utopia_Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-utopia_Tests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-utopia_Tests/Pods-utopia_Tests.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
AD58C60D33BF3DE957B80316 /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = "<group>"; };
|
||||
B099FDB815DE6B171E39CF89 /* Pods_utopia_Tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_utopia_Tests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
B60E758ED76098E52DACF178 /* Pods-utopia_Example.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-utopia_Example.debug.xcconfig"; path = "Pods/Target Support Files/Pods-utopia_Example/Pods-utopia_Example.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
@@ -78,10 +72,6 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
607FACD51AFB9204008FA782 /* AppDelegate.swift */,
|
||||
607FACD71AFB9204008FA782 /* ViewController.swift */,
|
||||
607FACD91AFB9204008FA782 /* Main.storyboard */,
|
||||
607FACDC1AFB9204008FA782 /* Images.xcassets */,
|
||||
607FACDE1AFB9204008FA782 /* LaunchScreen.xib */,
|
||||
607FACD31AFB9204008FA782 /* Supporting Files */,
|
||||
);
|
||||
name = "Example for utopia";
|
||||
@@ -91,6 +81,8 @@
|
||||
607FACD31AFB9204008FA782 /* Supporting Files */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
607FACDC1AFB9204008FA782 /* Images.xcassets */,
|
||||
607FACDE1AFB9204008FA782 /* LaunchScreen.xib */,
|
||||
607FACD41AFB9204008FA782 /* Info.plist */,
|
||||
);
|
||||
name = "Supporting Files";
|
||||
@@ -111,8 +103,6 @@
|
||||
children = (
|
||||
B60E758ED76098E52DACF178 /* Pods-utopia_Example.debug.xcconfig */,
|
||||
DFFC9C07CDE48E9551AEAD4E /* Pods-utopia_Example.release.xcconfig */,
|
||||
6F466667E3C01068D5F3BABF /* Pods-utopia_Tests.debug.xcconfig */,
|
||||
3D6E6CE2CA9E2A4ED29C9C80 /* Pods-utopia_Tests.release.xcconfig */,
|
||||
);
|
||||
name = Pods;
|
||||
sourceTree = "<group>";
|
||||
@@ -146,18 +136,18 @@
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastSwiftUpdateCheck = 0830;
|
||||
LastUpgradeCheck = 0830;
|
||||
LastUpgradeCheck = 1110;
|
||||
ORGANIZATIONNAME = CocoaPods;
|
||||
TargetAttributes = {
|
||||
607FACCF1AFB9204008FA782 = {
|
||||
CreatedOnToolsVersion = 6.3.1;
|
||||
LastSwiftMigration = 0900;
|
||||
LastSwiftMigration = 1110;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 607FACCB1AFB9204008FA782 /* Build configuration list for PBXProject "utopia" */;
|
||||
compatibilityVersion = "Xcode 3.2";
|
||||
developmentRegion = English;
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
@@ -178,7 +168,6 @@
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
607FACDB1AFB9204008FA782 /* Main.storyboard in Resources */,
|
||||
607FACE01AFB9204008FA782 /* LaunchScreen.xib in Resources */,
|
||||
607FACDD1AFB9204008FA782 /* Images.xcassets in Resources */,
|
||||
);
|
||||
@@ -193,7 +182,7 @@
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${SRCROOT}/Pods/Target Support Files/Pods-utopia_Example/Pods-utopia_Example-frameworks.sh",
|
||||
"${PODS_ROOT}/Target Support Files/Pods-utopia_Example/Pods-utopia_Example-frameworks.sh",
|
||||
"${BUILT_PRODUCTS_DIR}/utopia/utopia.framework",
|
||||
);
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
@@ -202,7 +191,7 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-utopia_Example/Pods-utopia_Example-frameworks.sh\"\n";
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-utopia_Example/Pods-utopia_Example-frameworks.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
466881BFDDF35CB453DC3935 /* [CP] Check Pods Manifest.lock */ = {
|
||||
@@ -230,7 +219,6 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
607FACD81AFB9204008FA782 /* ViewController.swift in Sources */,
|
||||
607FACD61AFB9204008FA782 /* AppDelegate.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -238,14 +226,6 @@
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
607FACD91AFB9204008FA782 /* Main.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
607FACDA1AFB9204008FA782 /* Base */,
|
||||
);
|
||||
name = Main.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
607FACDE1AFB9204008FA782 /* LaunchScreen.xib */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
@@ -261,6 +241,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
@@ -269,12 +250,14 @@
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
@@ -314,6 +297,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
@@ -322,12 +306,14 @@
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
@@ -361,13 +347,13 @@
|
||||
baseConfigurationReference = B60E758ED76098E52DACF178 /* Pods-utopia_Example.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
INFOPLIST_FILE = utopia/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
MODULE_NAME = ExampleApp;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
|
||||
SWIFT_VERSION = 4.0;
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
@@ -376,13 +362,13 @@
|
||||
baseConfigurationReference = DFFC9C07CDE48E9551AEAD4E /* Pods-utopia_Example.release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
INFOPLIST_FILE = utopia/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
MODULE_NAME = ExampleApp;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
|
||||
SWIFT_VERSION = 4.0;
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0900"
|
||||
LastUpgradeVersion = "1110"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
@@ -40,8 +40,16 @@
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
language = ""
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "607FACCF1AFB9204008FA782"
|
||||
BuildableName = "utopia_Example.app"
|
||||
BlueprintName = "utopia_Example"
|
||||
ReferencedContainer = "container:utopia.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
@@ -54,23 +62,11 @@
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "607FACCF1AFB9204008FA782"
|
||||
BuildableName = "utopia_Example.app"
|
||||
BlueprintName = "utopia_Example"
|
||||
ReferencedContainer = "container:utopia.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
language = ""
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
@@ -87,8 +83,6 @@
|
||||
ReferencedContainer = "container:utopia.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
|
||||
@@ -1,11 +1,3 @@
|
||||
//
|
||||
// AppDelegate.swift
|
||||
// utopia
|
||||
//
|
||||
// Created by Dmitriy Kalachev on 04/09/2018.
|
||||
// Copyright (c) 2018 Dmitriy Kalachev. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
@UIApplicationMain
|
||||
@@ -13,34 +5,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
|
||||
var window: UIWindow?
|
||||
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
|
||||
// Override point for customization after application launch.
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func applicationWillResignActive(_ application: UIApplication) {
|
||||
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
|
||||
// Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
|
||||
}
|
||||
|
||||
func applicationDidEnterBackground(_ application: UIApplication) {
|
||||
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
|
||||
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
|
||||
}
|
||||
|
||||
func applicationWillEnterForeground(_ application: UIApplication) {
|
||||
// Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background.
|
||||
}
|
||||
|
||||
func applicationDidBecomeActive(_ application: UIApplication) {
|
||||
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
|
||||
}
|
||||
|
||||
func applicationWillTerminate(_ application: UIApplication) {
|
||||
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13771" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="vXZ-lx-hvc">
|
||||
<device id="retina4_7" orientation="portrait">
|
||||
<adaptation id="fullscreen"/>
|
||||
</device>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13772"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="ufC-wZ-h7g">
|
||||
<objects>
|
||||
<viewController id="vXZ-lx-hvc" customClass="ViewController" customModule="utopia_Example" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<layoutGuides>
|
||||
<viewControllerLayoutGuide type="top" id="jyV-Pf-zRb"/>
|
||||
<viewControllerLayoutGuide type="bottom" id="2fi-mo-0CV"/>
|
||||
</layoutGuides>
|
||||
<view key="view" contentMode="scaleToFill" id="kh9-bI-dsS">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="x5A-6p-PRh" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
@@ -24,8 +24,6 @@
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>armv7</string>
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
//
|
||||
// ViewController.swift
|
||||
// utopia
|
||||
//
|
||||
// Created by Dmitriy Kalachev on 04/09/2018.
|
||||
// Copyright (c) 2018 Dmitriy Kalachev. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class ViewController: UIViewController {
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
// Do any additional setup after loading the view, typically from a nib.
|
||||
}
|
||||
|
||||
override func didReceiveMemoryWarning() {
|
||||
super.didReceiveMemoryWarning()
|
||||
// Dispose of any resources that can be recreated.
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+3
-34
@@ -1,42 +1,11 @@
|
||||
#
|
||||
# Be sure to run `pod lib lint utopia.podspec' to ensure this is a
|
||||
# valid spec before submitting.
|
||||
#
|
||||
# Any lines starting with a # are optional, but their use is encouraged
|
||||
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html
|
||||
#
|
||||
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'utopia'
|
||||
s.version = '0.1.0'
|
||||
s.summary = 'A short description of utopia.'
|
||||
|
||||
# This description is used to generate tags and improve search results.
|
||||
# * Think: What does it do? Why did you write it? What is the focus?
|
||||
# * Try to keep it short, snappy and to the point.
|
||||
# * Write the description between the DESC delimiters below.
|
||||
# * Finally, don't worry about the indent, CocoaPods strips it!
|
||||
|
||||
s.description = <<-DESC
|
||||
TODO: Add long description of the pod here.
|
||||
DESC
|
||||
|
||||
s.version = '0.2.1'
|
||||
s.summary = 'Common utilities for Swift projects'
|
||||
s.homepage = 'https://github.com/Ramotion/utopia'
|
||||
# s.screenshots = 'www.example.com/screenshots_1', 'www.example.com/screenshots_2'
|
||||
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
||||
s.author = { 'Dmitriy Kalachev' => 'dima.k@ramotion.com' }
|
||||
s.author = { 'Ramotion' => 'igor.k@ramotion.com' }
|
||||
s.source = { :git => 'https://github.com/Ramotion/utopia.git', :tag => s.version.to_s }
|
||||
# s.social_media_url = 'https://twitter.com/<TWITTER_USERNAME>'
|
||||
|
||||
s.ios.deployment_target = '10.0'
|
||||
|
||||
s.source_files = 'utopia/Source/**/*'
|
||||
|
||||
# s.resource_bundles = {
|
||||
# 'utopia' => ['utopia/Assets/*.png']
|
||||
# }
|
||||
|
||||
# s.public_header_files = 'Pod/Source/**/*.h'
|
||||
# s.frameworks = 'UIKit', 'MapKit'
|
||||
# s.dependency 'AFNetworking', '~> 2.3'
|
||||
end
|
||||
|
||||
+4
-4
@@ -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
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
//
|
||||
// This source file is largely a copy of code from Swift.org open source project's
|
||||
// files JSONEncoder.swift and Codeable.swift.
|
||||
//
|
||||
// Unfortunately those files do not expose the internal _JSONEncoder and
|
||||
// _JSONDecoder classes, which are in fact dictionary encoder/decoders and
|
||||
// precisely what we want...
|
||||
//
|
||||
// The original code is copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors
|
||||
// Licensed under Apache License v2.0 with Runtime Library Exception
|
||||
//
|
||||
// See https://swift.org/LICENSE.txt for license information
|
||||
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
|
||||
//
|
||||
// Modifications and additional code here is copyright (c) 2018 Sam Deane, and
|
||||
// is licensed under the same terms.
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
import Foundation
|
||||
|
||||
internal struct DictionaryCodingKey : CodingKey {
|
||||
public var stringValue: String
|
||||
public var intValue: Int?
|
||||
|
||||
public init?(stringValue: String) {
|
||||
self.stringValue = stringValue
|
||||
self.intValue = nil
|
||||
}
|
||||
|
||||
public init?(intValue: Int) {
|
||||
self.stringValue = "\(intValue)"
|
||||
self.intValue = intValue
|
||||
}
|
||||
|
||||
public init(stringValue: String, intValue: Int?) {
|
||||
self.stringValue = stringValue
|
||||
self.intValue = intValue
|
||||
}
|
||||
|
||||
internal init(index: Int) {
|
||||
self.stringValue = "Index \(index)"
|
||||
self.intValue = index
|
||||
}
|
||||
|
||||
internal static let `super` = DictionaryCodingKey(stringValue: "super")!
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,915 +0,0 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
//
|
||||
// This source file is largely a copy of code from Swift.org open source project's
|
||||
// files JSONEncoder.swift and Codeable.swift.
|
||||
//
|
||||
// Unfortunately those files do not expose the internal _JSONEncoder and
|
||||
// _JSONDecoder classes, which are in fact dictionary encoder/decoders and
|
||||
// precisely what we want...
|
||||
//
|
||||
// The original code is copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors
|
||||
// Licensed under Apache License v2.0 with Runtime Library Exception
|
||||
//
|
||||
// See https://swift.org/LICENSE.txt for license information
|
||||
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
|
||||
//
|
||||
// Modifications and additional code here is copyright (c) 2018 Sam Deane, and
|
||||
// is licensed under the same terms.
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
import Foundation
|
||||
|
||||
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Dictionary Encoder
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
/// `DictionaryEncoder` facilitates the encoding of `Encodable` values into Dictionary.
|
||||
open class DictionaryEncoder {
|
||||
// MARK: Options
|
||||
|
||||
/// The strategy to use for encoding `Date` values.
|
||||
public enum DateEncodingStrategy {
|
||||
/// Defer to `Date` for choosing an encoding. This is the default strategy.
|
||||
case deferredToDate
|
||||
|
||||
/// Encode the `Date` as a UNIX timestamp (as a Dictionary number).
|
||||
case secondsSince1970
|
||||
|
||||
/// Encode the `Date` as UNIX millisecond timestamp (as a Dictionary number).
|
||||
case millisecondsSince1970
|
||||
|
||||
/// Encode the `Date` as an ISO-8601-formatted string (in RFC 3339 format).
|
||||
@available(OSX 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *)
|
||||
case iso8601
|
||||
|
||||
/// Encode the `Date` as a string formatted by the given formatter.
|
||||
case formatted(DateFormatter)
|
||||
|
||||
/// Encode the `Date` as a custom value encoded by the given closure.
|
||||
///
|
||||
/// If the closure fails to encode a value into the given encoder, the encoder will encode an empty automatic container in its place.
|
||||
case custom((Date, Encoder) throws -> Void)
|
||||
}
|
||||
|
||||
/// The strategy to use for encoding `Data` values.
|
||||
public enum DataEncodingStrategy {
|
||||
/// Defer to `Data` for choosing an encoding.
|
||||
case deferredToData
|
||||
|
||||
/// Encoded the `Data` as a Base64-encoded string. This is the default strategy.
|
||||
case base64
|
||||
|
||||
/// Encode the `Data` as a custom value encoded by the given closure.
|
||||
///
|
||||
/// If the closure fails to encode a value into the given encoder, the encoder will encode an empty automatic container in its place.
|
||||
case custom((Data, Encoder) throws -> Void)
|
||||
}
|
||||
|
||||
/// The strategy to use for non-Dictionary-conforming floating-point values (IEEE 754 infinity and NaN).
|
||||
public enum NonConformingFloatEncodingStrategy {
|
||||
/// Throw upon encountering non-conforming values. This is the default strategy.
|
||||
case `throw`
|
||||
|
||||
/// Encode the values using the given representation strings.
|
||||
case convertToString(positiveInfinity: String, negativeInfinity: String, nan: String)
|
||||
}
|
||||
|
||||
/// The strategy to use for automatically changing the value of keys before encoding.
|
||||
public enum KeyEncodingStrategy {
|
||||
/// Use the keys specified by each type. This is the default strategy.
|
||||
case useDefaultKeys
|
||||
|
||||
/// Convert from "camelCaseKeys" to "snake_case_keys" before writing a key to Dictionary payload.
|
||||
///
|
||||
/// Capital characters are determined by testing membership in `CharacterSet.uppercaseLetters` and `CharacterSet.lowercaseLetters` (Unicode General Categories Lu and Lt).
|
||||
/// The conversion to lower case uses `Locale.system`, also known as the ICU "root" locale. This means the result is consistent regardless of the current user's locale and language preferences.
|
||||
///
|
||||
/// Converting from camel case to snake case:
|
||||
/// 1. Splits words at the boundary of lower-case to upper-case
|
||||
/// 2. Inserts `_` between words
|
||||
/// 3. Lowercases the entire string
|
||||
/// 4. Preserves starting and ending `_`.
|
||||
///
|
||||
/// For example, `oneTwoThree` becomes `one_two_three`. `_oneTwoThree_` becomes `_one_two_three_`.
|
||||
///
|
||||
/// - Note: Using a key encoding strategy has a nominal performance cost, as each string key has to be converted.
|
||||
case convertToSnakeCase
|
||||
|
||||
/// Provide a custom conversion to the key in the encoded Dictionary from the keys specified by the encoded types.
|
||||
/// The full path to the current encoding position is provided for context (in case you need to locate this key within the payload). The returned key is used in place of the last component in the coding path before encoding.
|
||||
/// If the result of the conversion is a duplicate key, then only one value will be present in the result.
|
||||
case custom((_ codingPath: [CodingKey]) -> CodingKey)
|
||||
|
||||
internal static func _convertToSnakeCase(_ stringKey: String) -> String {
|
||||
guard stringKey.count > 0 else { return stringKey }
|
||||
|
||||
var words : [Range<String.Index>] = []
|
||||
// The general idea of this algorithm is to split words on transition from lower to upper case, then on transition of >1 upper case characters to lowercase
|
||||
//
|
||||
// myProperty -> my_property
|
||||
// myURLProperty -> my_url_property
|
||||
//
|
||||
// We assume, per Swift naming conventions, that the first character of the key is lowercase.
|
||||
var wordStart = stringKey.startIndex
|
||||
var searchRange = stringKey.index(after: wordStart)..<stringKey.endIndex
|
||||
|
||||
// Find next uppercase character
|
||||
while let upperCaseRange = stringKey.rangeOfCharacter(from: CharacterSet.uppercaseLetters, options: [], range: searchRange) {
|
||||
let untilUpperCase = wordStart..<upperCaseRange.lowerBound
|
||||
words.append(untilUpperCase)
|
||||
|
||||
// Find next lowercase character
|
||||
searchRange = upperCaseRange.lowerBound..<searchRange.upperBound
|
||||
guard let lowerCaseRange = stringKey.rangeOfCharacter(from: CharacterSet.lowercaseLetters, options: [], range: searchRange) else {
|
||||
// There are no more lower case letters. Just end here.
|
||||
wordStart = searchRange.lowerBound
|
||||
break
|
||||
}
|
||||
|
||||
// Is the next lowercase letter more than 1 after the uppercase? If so, we encountered a group of uppercase letters that we should treat as its own word
|
||||
let nextCharacterAfterCapital = stringKey.index(after: upperCaseRange.lowerBound)
|
||||
if lowerCaseRange.lowerBound == nextCharacterAfterCapital {
|
||||
// The next character after capital is a lower case character and therefore not a word boundary.
|
||||
// Continue searching for the next upper case for the boundary.
|
||||
wordStart = upperCaseRange.lowerBound
|
||||
} else {
|
||||
// There was a range of >1 capital letters. Turn those into a word, stopping at the capital before the lower case character.
|
||||
let beforeLowerIndex = stringKey.index(before: lowerCaseRange.lowerBound)
|
||||
words.append(upperCaseRange.lowerBound..<beforeLowerIndex)
|
||||
|
||||
// Next word starts at the capital before the lowercase we just found
|
||||
wordStart = beforeLowerIndex
|
||||
}
|
||||
searchRange = lowerCaseRange.upperBound..<searchRange.upperBound
|
||||
}
|
||||
words.append(wordStart..<searchRange.upperBound)
|
||||
let result = words.map({ (range) in
|
||||
return stringKey[range].lowercased()
|
||||
}).joined(separator: "_")
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
/// The strategy to use in encoding dates. Defaults to `.deferredToDate`.
|
||||
open var dateEncodingStrategy: DateEncodingStrategy = .deferredToDate
|
||||
|
||||
/// The strategy to use in encoding binary data. Defaults to `.base64`.
|
||||
open var dataEncodingStrategy: DataEncodingStrategy = .base64
|
||||
|
||||
/// The strategy to use in encoding non-conforming numbers. Defaults to `.throw`.
|
||||
open var nonConformingFloatEncodingStrategy: NonConformingFloatEncodingStrategy = .throw
|
||||
|
||||
/// The strategy to use for encoding keys. Defaults to `.useDefaultKeys`.
|
||||
open var keyEncodingStrategy: KeyEncodingStrategy = .useDefaultKeys
|
||||
|
||||
/// Contextual user-provided information for use during encoding.
|
||||
open var userInfo: [CodingUserInfoKey : Any] = [:]
|
||||
|
||||
/// Options set on the top-level encoder to pass down the encoding hierarchy.
|
||||
fileprivate struct _Options {
|
||||
let dateEncodingStrategy: DateEncodingStrategy
|
||||
let dataEncodingStrategy: DataEncodingStrategy
|
||||
let nonConformingFloatEncodingStrategy: NonConformingFloatEncodingStrategy
|
||||
let keyEncodingStrategy: KeyEncodingStrategy
|
||||
let userInfo: [CodingUserInfoKey : Any]
|
||||
}
|
||||
|
||||
/// The options set on the top-level encoder.
|
||||
fileprivate var options: _Options {
|
||||
return _Options(dateEncodingStrategy: dateEncodingStrategy,
|
||||
dataEncodingStrategy: dataEncodingStrategy,
|
||||
nonConformingFloatEncodingStrategy: nonConformingFloatEncodingStrategy,
|
||||
keyEncodingStrategy: keyEncodingStrategy,
|
||||
userInfo: userInfo)
|
||||
}
|
||||
|
||||
// MARK: - Constructing a Dictionary Encoder
|
||||
/// Initializes `self` with default strategies.
|
||||
public init() {}
|
||||
|
||||
// MARK: - Encoding Values
|
||||
/// Encodes the given top-level value and returns its Dictionary representation.
|
||||
///
|
||||
/// - parameter value: The value to encode.
|
||||
/// - returns: A new `Data` value containing the encoded Dictionary data.
|
||||
/// - throws: `EncodingError.invalidValue` if a non-conforming floating-point value is encountered during encoding, and the encoding strategy is `.throw`.
|
||||
/// - throws: An error if any value throws an error during encoding.
|
||||
open func encode<T : Encodable>(_ value: T) throws -> NSDictionary {
|
||||
let encoder = _DictionaryEncoder(options: self.options)
|
||||
|
||||
guard let topLevel = try encoder.box_(value) else {
|
||||
throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: [], debugDescription: "Top-level \(T.self) did not encode any values."))
|
||||
}
|
||||
|
||||
if topLevel is NSNull {
|
||||
throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: [], debugDescription: "Top-level \(T.self) encoded as null Dictionary fragment."))
|
||||
} else if topLevel is NSNumber {
|
||||
throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: [], debugDescription: "Top-level \(T.self) encoded as number Dictionary fragment."))
|
||||
} else if topLevel is NSString {
|
||||
throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: [], debugDescription: "Top-level \(T.self) encoded as string Dictionary fragment."))
|
||||
}
|
||||
|
||||
return topLevel as! NSDictionary
|
||||
}
|
||||
|
||||
// MARK: - Encoding Values
|
||||
/// Encodes the given top-level value and returns its Dictionary representation.
|
||||
///
|
||||
/// - parameter value: The value to encode.
|
||||
/// - returns: A new `Data` value containing the encoded Dictionary data.
|
||||
/// - throws: `EncodingError.invalidValue` if a non-conforming floating-point value is encountered during encoding, and the encoding strategy is `.throw`.
|
||||
/// - throws: An error if any value throws an error during encoding.
|
||||
open func encode<T : Encodable>(_ value: T) throws -> [String:Any] {
|
||||
let encoder = _DictionaryEncoder(options: self.options)
|
||||
|
||||
guard let topLevel = try encoder.box_(value) else {
|
||||
throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: [], debugDescription: "Top-level \(T.self) did not encode any values."))
|
||||
}
|
||||
|
||||
if topLevel is NSNull {
|
||||
throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: [], debugDescription: "Top-level \(T.self) encoded as null Dictionary fragment."))
|
||||
} else if topLevel is NSNumber {
|
||||
throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: [], debugDescription: "Top-level \(T.self) encoded as number Dictionary fragment."))
|
||||
} else if topLevel is NSString {
|
||||
throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: [], debugDescription: "Top-level \(T.self) encoded as string Dictionary fragment."))
|
||||
}
|
||||
|
||||
return topLevel as! [String:Any]
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - _DictionaryEncoder
|
||||
fileprivate class _DictionaryEncoder : Encoder {
|
||||
// MARK: Properties
|
||||
/// The encoder's storage.
|
||||
fileprivate var storage: _DictionaryEncodingStorage
|
||||
|
||||
/// Options set on the top-level encoder.
|
||||
fileprivate let options: DictionaryEncoder._Options
|
||||
|
||||
/// The path to the current point in encoding.
|
||||
public var codingPath: [CodingKey]
|
||||
|
||||
/// Contextual user-provided information for use during encoding.
|
||||
public var userInfo: [CodingUserInfoKey : Any] {
|
||||
return self.options.userInfo
|
||||
}
|
||||
|
||||
// MARK: - Initialization
|
||||
/// Initializes `self` with the given top-level encoder options.
|
||||
fileprivate init(options: DictionaryEncoder._Options, codingPath: [CodingKey] = []) {
|
||||
self.options = options
|
||||
self.storage = _DictionaryEncodingStorage()
|
||||
self.codingPath = codingPath
|
||||
}
|
||||
|
||||
/// Returns whether a new element can be encoded at this coding path.
|
||||
///
|
||||
/// `true` if an element has not yet been encoded at this coding path; `false` otherwise.
|
||||
fileprivate var canEncodeNewValue: Bool {
|
||||
// Every time a new value gets encoded, the key it's encoded for is pushed onto the coding path (even if it's a nil key from an unkeyed container).
|
||||
// At the same time, every time a container is requested, a new value gets pushed onto the storage stack.
|
||||
// If there are more values on the storage stack than on the coding path, it means the value is requesting more than one container, which violates the precondition.
|
||||
//
|
||||
// This means that anytime something that can request a new container goes onto the stack, we MUST push a key onto the coding path.
|
||||
// Things which will not request containers do not need to have the coding path extended for them (but it doesn't matter if it is, because they will not reach here).
|
||||
return self.storage.count == self.codingPath.count
|
||||
}
|
||||
|
||||
// MARK: - Encoder Methods
|
||||
public func container<Key>(keyedBy: Key.Type) -> KeyedEncodingContainer<Key> {
|
||||
// If an existing keyed container was already requested, return that one.
|
||||
let topContainer: NSMutableDictionary
|
||||
if self.canEncodeNewValue {
|
||||
// We haven't yet pushed a container at this level; do so here.
|
||||
topContainer = self.storage.pushKeyedContainer()
|
||||
} else {
|
||||
guard let container = self.storage.containers.last as? NSMutableDictionary else {
|
||||
preconditionFailure("Attempt to push new keyed encoding container when already previously encoded at this path.")
|
||||
}
|
||||
|
||||
topContainer = container
|
||||
}
|
||||
|
||||
let container = DictionaryCodingKeyedEncodingContainer<Key>(referencing: self, codingPath: self.codingPath, wrapping: topContainer)
|
||||
return KeyedEncodingContainer(container)
|
||||
}
|
||||
|
||||
public func unkeyedContainer() -> UnkeyedEncodingContainer {
|
||||
// If an existing unkeyed container was already requested, return that one.
|
||||
let topContainer: NSMutableArray
|
||||
if self.canEncodeNewValue {
|
||||
// We haven't yet pushed a container at this level; do so here.
|
||||
topContainer = self.storage.pushUnkeyedContainer()
|
||||
} else {
|
||||
guard let container = self.storage.containers.last as? NSMutableArray else {
|
||||
preconditionFailure("Attempt to push new unkeyed encoding container when already previously encoded at this path.")
|
||||
}
|
||||
|
||||
topContainer = container
|
||||
}
|
||||
|
||||
return _DictionaryUnkeyedEncodingContainer(referencing: self, codingPath: self.codingPath, wrapping: topContainer)
|
||||
}
|
||||
|
||||
public func singleValueContainer() -> SingleValueEncodingContainer {
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Encoding Storage and Containers
|
||||
fileprivate struct _DictionaryEncodingStorage {
|
||||
// MARK: Properties
|
||||
/// The container stack.
|
||||
/// Elements may be any one of the Dictionary types (NSNull, NSNumber, NSString, NSArray, NSDictionary).
|
||||
private(set) fileprivate var containers: [NSObject] = []
|
||||
|
||||
// MARK: - Initialization
|
||||
/// Initializes `self` with no containers.
|
||||
fileprivate init() {}
|
||||
|
||||
// MARK: - Modifying the Stack
|
||||
fileprivate var count: Int {
|
||||
return self.containers.count
|
||||
}
|
||||
|
||||
fileprivate mutating func pushKeyedContainer() -> NSMutableDictionary {
|
||||
let dictionary = NSMutableDictionary()
|
||||
self.containers.append(dictionary)
|
||||
return dictionary
|
||||
}
|
||||
|
||||
fileprivate mutating func pushUnkeyedContainer() -> NSMutableArray {
|
||||
let array = NSMutableArray()
|
||||
self.containers.append(array)
|
||||
return array
|
||||
}
|
||||
|
||||
fileprivate mutating func push(container: NSObject) {
|
||||
self.containers.append(container)
|
||||
}
|
||||
|
||||
fileprivate mutating func popContainer() -> NSObject {
|
||||
precondition(self.containers.count > 0, "Empty container stack.")
|
||||
return self.containers.popLast()!
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Encoding Containers
|
||||
fileprivate struct DictionaryCodingKeyedEncodingContainer<K : CodingKey> : KeyedEncodingContainerProtocol {
|
||||
typealias Key = K
|
||||
|
||||
// MARK: Properties
|
||||
/// A reference to the encoder we're writing to.
|
||||
private let encoder: _DictionaryEncoder
|
||||
|
||||
/// A reference to the container we're writing to.
|
||||
private let container: NSMutableDictionary
|
||||
|
||||
/// The path of coding keys taken to get to this point in encoding.
|
||||
private(set) public var codingPath: [CodingKey]
|
||||
|
||||
// MARK: - Initialization
|
||||
/// Initializes `self` with the given references.
|
||||
fileprivate init(referencing encoder: _DictionaryEncoder, codingPath: [CodingKey], wrapping container: NSMutableDictionary) {
|
||||
self.encoder = encoder
|
||||
self.codingPath = codingPath
|
||||
self.container = container
|
||||
}
|
||||
|
||||
// MARK: - Coding Path Operations
|
||||
private func _converted(_ key: CodingKey) -> CodingKey {
|
||||
switch encoder.options.keyEncodingStrategy {
|
||||
case .useDefaultKeys:
|
||||
return key
|
||||
case .convertToSnakeCase:
|
||||
let newKeyString = DictionaryEncoder.KeyEncodingStrategy._convertToSnakeCase(key.stringValue)
|
||||
return DictionaryCodingKey(stringValue: newKeyString, intValue: key.intValue)
|
||||
case .custom(let converter):
|
||||
return converter(codingPath + [key])
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - KeyedEncodingContainerProtocol Methods
|
||||
public mutating func encodeNil(forKey key: Key) throws {
|
||||
self.container[_converted(key).stringValue] = NSNull()
|
||||
}
|
||||
public mutating func encode(_ value: Bool, forKey key: Key) throws {
|
||||
self.container[_converted(key).stringValue] = self.encoder.box(value)
|
||||
}
|
||||
public mutating func encode(_ value: Int, forKey key: Key) throws {
|
||||
self.container[_converted(key).stringValue] = self.encoder.box(value)
|
||||
}
|
||||
public mutating func encode(_ value: Int8, forKey key: Key) throws {
|
||||
self.container[_converted(key).stringValue] = self.encoder.box(value)
|
||||
}
|
||||
public mutating func encode(_ value: Int16, forKey key: Key) throws {
|
||||
self.container[_converted(key).stringValue] = self.encoder.box(value)
|
||||
}
|
||||
public mutating func encode(_ value: Int32, forKey key: Key) throws {
|
||||
self.container[_converted(key).stringValue] = self.encoder.box(value)
|
||||
}
|
||||
public mutating func encode(_ value: Int64, forKey key: Key) throws {
|
||||
self.container[_converted(key).stringValue] = self.encoder.box(value)
|
||||
}
|
||||
public mutating func encode(_ value: UInt, forKey key: Key) throws {
|
||||
self.container[_converted(key).stringValue] = self.encoder.box(value)
|
||||
}
|
||||
public mutating func encode(_ value: UInt8, forKey key: Key) throws {
|
||||
self.container[_converted(key).stringValue] = self.encoder.box(value)
|
||||
}
|
||||
public mutating func encode(_ value: UInt16, forKey key: Key) throws {
|
||||
self.container[_converted(key).stringValue] = self.encoder.box(value)
|
||||
}
|
||||
public mutating func encode(_ value: UInt32, forKey key: Key) throws {
|
||||
self.container[_converted(key).stringValue] = self.encoder.box(value)
|
||||
}
|
||||
public mutating func encode(_ value: UInt64, forKey key: Key) throws {
|
||||
self.container[_converted(key).stringValue] = self.encoder.box(value)
|
||||
}
|
||||
public mutating func encode(_ value: String, forKey key: Key) throws {
|
||||
self.container[_converted(key).stringValue] = self.encoder.box(value)
|
||||
}
|
||||
|
||||
public mutating func encode(_ value: Float, forKey key: Key) throws {
|
||||
// Since the float may be invalid and throw, the coding path needs to contain this key.
|
||||
self.encoder.codingPath.append(key)
|
||||
defer { self.encoder.codingPath.removeLast() }
|
||||
self.container[_converted(key).stringValue] = try self.encoder.box(value)
|
||||
}
|
||||
|
||||
public mutating func encode(_ value: Double, forKey key: Key) throws {
|
||||
// Since the double may be invalid and throw, the coding path needs to contain this key.
|
||||
self.encoder.codingPath.append(key)
|
||||
defer { self.encoder.codingPath.removeLast() }
|
||||
self.container[_converted(key).stringValue] = try self.encoder.box(value)
|
||||
}
|
||||
|
||||
public mutating func encode<T : Encodable>(_ value: T, forKey key: Key) throws {
|
||||
self.encoder.codingPath.append(key)
|
||||
defer { self.encoder.codingPath.removeLast() }
|
||||
self.container[_converted(key).stringValue] = try self.encoder.box(value)
|
||||
}
|
||||
|
||||
public mutating func nestedContainer<NestedKey>(keyedBy keyType: NestedKey.Type, forKey key: Key) -> KeyedEncodingContainer<NestedKey> {
|
||||
let dictionary = NSMutableDictionary()
|
||||
self.container[_converted(key).stringValue] = dictionary
|
||||
|
||||
self.codingPath.append(key)
|
||||
defer { self.codingPath.removeLast() }
|
||||
|
||||
let container = DictionaryCodingKeyedEncodingContainer<NestedKey>(referencing: self.encoder, codingPath: self.codingPath, wrapping: dictionary)
|
||||
return KeyedEncodingContainer(container)
|
||||
}
|
||||
|
||||
public mutating func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer {
|
||||
let array = NSMutableArray()
|
||||
self.container[_converted(key).stringValue] = array
|
||||
|
||||
self.codingPath.append(key)
|
||||
defer { self.codingPath.removeLast() }
|
||||
return _DictionaryUnkeyedEncodingContainer(referencing: self.encoder, codingPath: self.codingPath, wrapping: array)
|
||||
}
|
||||
|
||||
public mutating func superEncoder() -> Encoder {
|
||||
return _DictionaryReferencingEncoder(referencing: self.encoder, key: DictionaryCodingKey.super, convertedKey: _converted(DictionaryCodingKey.super), wrapping: self.container)
|
||||
}
|
||||
|
||||
public mutating func superEncoder(forKey key: Key) -> Encoder {
|
||||
return _DictionaryReferencingEncoder(referencing: self.encoder, key: key, convertedKey: _converted(key), wrapping: self.container)
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate struct _DictionaryUnkeyedEncodingContainer : UnkeyedEncodingContainer {
|
||||
// MARK: Properties
|
||||
/// A reference to the encoder we're writing to.
|
||||
private let encoder: _DictionaryEncoder
|
||||
|
||||
/// A reference to the container we're writing to.
|
||||
private let container: NSMutableArray
|
||||
|
||||
/// The path of coding keys taken to get to this point in encoding.
|
||||
private(set) public var codingPath: [CodingKey]
|
||||
|
||||
/// The number of elements encoded into the container.
|
||||
public var count: Int {
|
||||
return self.container.count
|
||||
}
|
||||
|
||||
// MARK: - Initialization
|
||||
/// Initializes `self` with the given references.
|
||||
fileprivate init(referencing encoder: _DictionaryEncoder, codingPath: [CodingKey], wrapping container: NSMutableArray) {
|
||||
self.encoder = encoder
|
||||
self.codingPath = codingPath
|
||||
self.container = container
|
||||
}
|
||||
|
||||
// MARK: - UnkeyedEncodingContainer Methods
|
||||
public mutating func encodeNil() throws { self.container.add(NSNull()) }
|
||||
public mutating func encode(_ value: Bool) throws { self.container.add(self.encoder.box(value)) }
|
||||
public mutating func encode(_ value: Int) throws { self.container.add(self.encoder.box(value)) }
|
||||
public mutating func encode(_ value: Int8) throws { self.container.add(self.encoder.box(value)) }
|
||||
public mutating func encode(_ value: Int16) throws { self.container.add(self.encoder.box(value)) }
|
||||
public mutating func encode(_ value: Int32) throws { self.container.add(self.encoder.box(value)) }
|
||||
public mutating func encode(_ value: Int64) throws { self.container.add(self.encoder.box(value)) }
|
||||
public mutating func encode(_ value: UInt) throws { self.container.add(self.encoder.box(value)) }
|
||||
public mutating func encode(_ value: UInt8) throws { self.container.add(self.encoder.box(value)) }
|
||||
public mutating func encode(_ value: UInt16) throws { self.container.add(self.encoder.box(value)) }
|
||||
public mutating func encode(_ value: UInt32) throws { self.container.add(self.encoder.box(value)) }
|
||||
public mutating func encode(_ value: UInt64) throws { self.container.add(self.encoder.box(value)) }
|
||||
public mutating func encode(_ value: String) throws { self.container.add(self.encoder.box(value)) }
|
||||
|
||||
public mutating func encode(_ value: Float) throws {
|
||||
// Since the float may be invalid and throw, the coding path needs to contain this key.
|
||||
self.encoder.codingPath.append(DictionaryCodingKey(index: self.count))
|
||||
defer { self.encoder.codingPath.removeLast() }
|
||||
self.container.add(try self.encoder.box(value))
|
||||
}
|
||||
|
||||
public mutating func encode(_ value: Double) throws {
|
||||
// Since the double may be invalid and throw, the coding path needs to contain this key.
|
||||
self.encoder.codingPath.append(DictionaryCodingKey(index: self.count))
|
||||
defer { self.encoder.codingPath.removeLast() }
|
||||
self.container.add(try self.encoder.box(value))
|
||||
}
|
||||
|
||||
public mutating func encode<T : Encodable>(_ value: T) throws {
|
||||
self.encoder.codingPath.append(DictionaryCodingKey(index: self.count))
|
||||
defer { self.encoder.codingPath.removeLast() }
|
||||
self.container.add(try self.encoder.box(value))
|
||||
}
|
||||
|
||||
public mutating func nestedContainer<NestedKey>(keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer<NestedKey> {
|
||||
self.codingPath.append(DictionaryCodingKey(index: self.count))
|
||||
defer { self.codingPath.removeLast() }
|
||||
|
||||
let dictionary = NSMutableDictionary()
|
||||
self.container.add(dictionary)
|
||||
|
||||
let container = DictionaryCodingKeyedEncodingContainer<NestedKey>(referencing: self.encoder, codingPath: self.codingPath, wrapping: dictionary)
|
||||
return KeyedEncodingContainer(container)
|
||||
}
|
||||
|
||||
public mutating func nestedUnkeyedContainer() -> UnkeyedEncodingContainer {
|
||||
self.codingPath.append(DictionaryCodingKey(index: self.count))
|
||||
defer { self.codingPath.removeLast() }
|
||||
|
||||
let array = NSMutableArray()
|
||||
self.container.add(array)
|
||||
return _DictionaryUnkeyedEncodingContainer(referencing: self.encoder, codingPath: self.codingPath, wrapping: array)
|
||||
}
|
||||
|
||||
public mutating func superEncoder() -> Encoder {
|
||||
return _DictionaryReferencingEncoder(referencing: self.encoder, at: self.container.count, wrapping: self.container)
|
||||
}
|
||||
}
|
||||
|
||||
extension _DictionaryEncoder : SingleValueEncodingContainer {
|
||||
// MARK: - SingleValueEncodingContainer Methods
|
||||
fileprivate func assertCanEncodeNewValue() {
|
||||
precondition(self.canEncodeNewValue, "Attempt to encode value through single value container when previously value already encoded.")
|
||||
}
|
||||
|
||||
public func encodeNil() throws {
|
||||
assertCanEncodeNewValue()
|
||||
self.storage.push(container: NSNull())
|
||||
}
|
||||
|
||||
public func encode(_ value: Bool) throws {
|
||||
assertCanEncodeNewValue()
|
||||
self.storage.push(container: self.box(value))
|
||||
}
|
||||
|
||||
public func encode(_ value: Int) throws {
|
||||
assertCanEncodeNewValue()
|
||||
self.storage.push(container: self.box(value))
|
||||
}
|
||||
|
||||
public func encode(_ value: Int8) throws {
|
||||
assertCanEncodeNewValue()
|
||||
self.storage.push(container: self.box(value))
|
||||
}
|
||||
|
||||
public func encode(_ value: Int16) throws {
|
||||
assertCanEncodeNewValue()
|
||||
self.storage.push(container: self.box(value))
|
||||
}
|
||||
|
||||
public func encode(_ value: Int32) throws {
|
||||
assertCanEncodeNewValue()
|
||||
self.storage.push(container: self.box(value))
|
||||
}
|
||||
|
||||
public func encode(_ value: Int64) throws {
|
||||
assertCanEncodeNewValue()
|
||||
self.storage.push(container: self.box(value))
|
||||
}
|
||||
|
||||
public func encode(_ value: UInt) throws {
|
||||
assertCanEncodeNewValue()
|
||||
self.storage.push(container: self.box(value))
|
||||
}
|
||||
|
||||
public func encode(_ value: UInt8) throws {
|
||||
assertCanEncodeNewValue()
|
||||
self.storage.push(container: self.box(value))
|
||||
}
|
||||
|
||||
public func encode(_ value: UInt16) throws {
|
||||
assertCanEncodeNewValue()
|
||||
self.storage.push(container: self.box(value))
|
||||
}
|
||||
|
||||
public func encode(_ value: UInt32) throws {
|
||||
assertCanEncodeNewValue()
|
||||
self.storage.push(container: self.box(value))
|
||||
}
|
||||
|
||||
public func encode(_ value: UInt64) throws {
|
||||
assertCanEncodeNewValue()
|
||||
self.storage.push(container: self.box(value))
|
||||
}
|
||||
|
||||
public func encode(_ value: String) throws {
|
||||
assertCanEncodeNewValue()
|
||||
self.storage.push(container: self.box(value))
|
||||
}
|
||||
|
||||
public func encode(_ value: Float) throws {
|
||||
assertCanEncodeNewValue()
|
||||
try self.storage.push(container: self.box(value))
|
||||
}
|
||||
|
||||
public func encode(_ value: Double) throws {
|
||||
assertCanEncodeNewValue()
|
||||
try self.storage.push(container: self.box(value))
|
||||
}
|
||||
|
||||
public func encode<T : Encodable>(_ value: T) throws {
|
||||
assertCanEncodeNewValue()
|
||||
try self.storage.push(container: self.box(value))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Concrete Value Representations
|
||||
extension _DictionaryEncoder {
|
||||
/// Returns the given value boxed in a container appropriate for pushing onto the container stack.
|
||||
fileprivate func box(_ value: Bool) -> NSObject { return NSNumber(value: value) }
|
||||
fileprivate func box(_ value: Int) -> NSObject { return NSNumber(value: value) }
|
||||
fileprivate func box(_ value: Int8) -> NSObject { return NSNumber(value: value) }
|
||||
fileprivate func box(_ value: Int16) -> NSObject { return NSNumber(value: value) }
|
||||
fileprivate func box(_ value: Int32) -> NSObject { return NSNumber(value: value) }
|
||||
fileprivate func box(_ value: Int64) -> NSObject { return NSNumber(value: value) }
|
||||
fileprivate func box(_ value: UInt) -> NSObject { return NSNumber(value: value) }
|
||||
fileprivate func box(_ value: UInt8) -> NSObject { return NSNumber(value: value) }
|
||||
fileprivate func box(_ value: UInt16) -> NSObject { return NSNumber(value: value) }
|
||||
fileprivate func box(_ value: UInt32) -> NSObject { return NSNumber(value: value) }
|
||||
fileprivate func box(_ value: UInt64) -> NSObject { return NSNumber(value: value) }
|
||||
fileprivate func box(_ value: String) -> NSObject { return NSString(string: value) }
|
||||
|
||||
fileprivate func box(_ float: Float) throws -> NSObject {
|
||||
guard !float.isInfinite && !float.isNaN else {
|
||||
guard case let .convertToString(positiveInfinity: posInfString,
|
||||
negativeInfinity: negInfString,
|
||||
nan: nanString) = self.options.nonConformingFloatEncodingStrategy else {
|
||||
throw EncodingError._invalidFloatingPointValue(float, at: codingPath)
|
||||
}
|
||||
|
||||
if float == Float.infinity {
|
||||
return NSString(string: posInfString)
|
||||
} else if float == -Float.infinity {
|
||||
return NSString(string: negInfString)
|
||||
} else {
|
||||
return NSString(string: nanString)
|
||||
}
|
||||
}
|
||||
|
||||
return NSNumber(value: float)
|
||||
}
|
||||
|
||||
fileprivate func box(_ double: Double) throws -> NSObject {
|
||||
guard !double.isInfinite && !double.isNaN else {
|
||||
guard case let .convertToString(positiveInfinity: posInfString,
|
||||
negativeInfinity: negInfString,
|
||||
nan: nanString) = self.options.nonConformingFloatEncodingStrategy else {
|
||||
throw EncodingError._invalidFloatingPointValue(double, at: codingPath)
|
||||
}
|
||||
|
||||
if double == Double.infinity {
|
||||
return NSString(string: posInfString)
|
||||
} else if double == -Double.infinity {
|
||||
return NSString(string: negInfString)
|
||||
} else {
|
||||
return NSString(string: nanString)
|
||||
}
|
||||
}
|
||||
|
||||
return NSNumber(value: double)
|
||||
}
|
||||
|
||||
fileprivate func box(_ date: Date) throws -> NSObject {
|
||||
switch self.options.dateEncodingStrategy {
|
||||
case .deferredToDate:
|
||||
// Must be called with a surrounding with(pushedKey:) call.
|
||||
// Dates encode as single-value objects; this can't both throw and push a container, so no need to catch the error.
|
||||
try date.encode(to: self)
|
||||
return self.storage.popContainer()
|
||||
|
||||
case .secondsSince1970:
|
||||
return NSNumber(value: date.timeIntervalSince1970)
|
||||
|
||||
case .millisecondsSince1970:
|
||||
return NSNumber(value: 1000.0 * date.timeIntervalSince1970)
|
||||
|
||||
case .iso8601:
|
||||
if #available(OSX 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) {
|
||||
return NSString(string: _iso8601Formatter.string(from: date))
|
||||
} else {
|
||||
fatalError("ISO8601DateFormatter is unavailable on this platform.")
|
||||
}
|
||||
|
||||
case .formatted(let formatter):
|
||||
return NSString(string: formatter.string(from: date))
|
||||
|
||||
case .custom(let closure):
|
||||
let depth = self.storage.count
|
||||
do {
|
||||
try closure(date, self)
|
||||
} catch {
|
||||
// If the value pushed a container before throwing, pop it back off to restore state.
|
||||
if self.storage.count > depth {
|
||||
let _ = self.storage.popContainer()
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
|
||||
guard self.storage.count > depth else {
|
||||
// The closure didn't encode anything. Return the default keyed container.
|
||||
return NSDictionary()
|
||||
}
|
||||
|
||||
// We can pop because the closure encoded something.
|
||||
return self.storage.popContainer()
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func box(_ data: Data) throws -> NSObject {
|
||||
switch self.options.dataEncodingStrategy {
|
||||
case .deferredToData:
|
||||
// Must be called with a surrounding with(pushedKey:) call.
|
||||
let depth = self.storage.count
|
||||
do {
|
||||
try data.encode(to: self)
|
||||
} catch {
|
||||
// If the value pushed a container before throwing, pop it back off to restore state.
|
||||
// This shouldn't be possible for Data (which encodes as an array of bytes), but it can't hurt to catch a failure.
|
||||
if self.storage.count > depth {
|
||||
let _ = self.storage.popContainer()
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
|
||||
return self.storage.popContainer()
|
||||
|
||||
case .base64:
|
||||
return NSString(string: data.base64EncodedString())
|
||||
|
||||
case .custom(let closure):
|
||||
let depth = self.storage.count
|
||||
do {
|
||||
try closure(data, self)
|
||||
} catch {
|
||||
// If the value pushed a container before throwing, pop it back off to restore state.
|
||||
if self.storage.count > depth {
|
||||
let _ = self.storage.popContainer()
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
|
||||
guard self.storage.count > depth else {
|
||||
// The closure didn't encode anything. Return the default keyed container.
|
||||
return NSDictionary()
|
||||
}
|
||||
|
||||
// We can pop because the closure encoded something.
|
||||
return self.storage.popContainer()
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func box<T : Encodable>(_ value: T) throws -> NSObject {
|
||||
return try self.box_(value) ?? NSDictionary()
|
||||
}
|
||||
|
||||
// This method is called "box_" instead of "box" to disambiguate it from the overloads. Because the return type here is different from all of the "box" overloads (and is more general), any "box" calls in here would call back into "box" recursively instead of calling the appropriate overload, which is not what we want.
|
||||
fileprivate func box_<T : Encodable>(_ value: T) throws -> NSObject? {
|
||||
if T.self == Date.self || T.self == NSDate.self {
|
||||
// Respect Date encoding strategy
|
||||
return try self.box((value as! Date))
|
||||
} else if T.self == Data.self || T.self == NSData.self {
|
||||
// Respect Data encoding strategy
|
||||
return try self.box((value as! Data))
|
||||
} else if T.self == URL.self || T.self == NSURL.self {
|
||||
// Encode URLs as single strings.
|
||||
return self.box((value as! URL).absoluteString)
|
||||
} else if T.self == Decimal.self || T.self == NSDecimalNumber.self {
|
||||
// DictionarySerialization can natively handle NSDecimalNumber.
|
||||
return (value as! NSDecimalNumber)
|
||||
}
|
||||
|
||||
// The value should request a container from the _DictionaryEncoder.
|
||||
let depth = self.storage.count
|
||||
do {
|
||||
try value.encode(to: self)
|
||||
} catch {
|
||||
// If the value pushed a container before throwing, pop it back off to restore state.
|
||||
if self.storage.count > depth {
|
||||
let _ = self.storage.popContainer()
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
|
||||
// The top container should be a new container.
|
||||
guard self.storage.count > depth else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return self.storage.popContainer()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - _DictionaryReferencingEncoder
|
||||
/// _DictionaryReferencingEncoder is a special subclass of _DictionaryEncoder which has its own storage, but references the contents of a different encoder.
|
||||
/// It's used in superEncoder(), which returns a new encoder for encoding a superclass -- the lifetime of the encoder should not escape the scope it's created in, but it doesn't necessarily know when it's done being used (to write to the original container).
|
||||
fileprivate class _DictionaryReferencingEncoder : _DictionaryEncoder {
|
||||
// MARK: Reference types.
|
||||
/// The type of container we're referencing.
|
||||
private enum Reference {
|
||||
/// Referencing a specific index in an array container.
|
||||
case array(NSMutableArray, Int)
|
||||
|
||||
/// Referencing a specific key in a dictionary container.
|
||||
case dictionary(NSMutableDictionary, String)
|
||||
}
|
||||
|
||||
// MARK: - Properties
|
||||
/// The encoder we're referencing.
|
||||
fileprivate let encoder: _DictionaryEncoder
|
||||
|
||||
/// The container reference itself.
|
||||
private let reference: Reference
|
||||
|
||||
// MARK: - Initialization
|
||||
/// Initializes `self` by referencing the given array container in the given encoder.
|
||||
fileprivate init(referencing encoder: _DictionaryEncoder, at index: Int, wrapping array: NSMutableArray) {
|
||||
self.encoder = encoder
|
||||
self.reference = .array(array, index)
|
||||
super.init(options: encoder.options, codingPath: encoder.codingPath)
|
||||
|
||||
self.codingPath.append(DictionaryCodingKey(index: index))
|
||||
}
|
||||
|
||||
/// Initializes `self` by referencing the given dictionary container in the given encoder.
|
||||
fileprivate init(referencing encoder: _DictionaryEncoder,
|
||||
key: CodingKey, convertedKey: CodingKey, wrapping dictionary: NSMutableDictionary) {
|
||||
self.encoder = encoder
|
||||
self.reference = .dictionary(dictionary, convertedKey.stringValue)
|
||||
super.init(options: encoder.options, codingPath: encoder.codingPath)
|
||||
|
||||
self.codingPath.append(key)
|
||||
}
|
||||
|
||||
// MARK: - Coding Path Operations
|
||||
fileprivate override var canEncodeNewValue: Bool {
|
||||
// With a regular encoder, the storage and coding path grow together.
|
||||
// A referencing encoder, however, inherits its parents coding path, as well as the key it was created for.
|
||||
// We have to take this into account.
|
||||
return self.storage.count == self.codingPath.count - self.encoder.codingPath.count - 1
|
||||
}
|
||||
|
||||
// MARK: - Deinitialization
|
||||
// Finalizes `self` by writing the contents of our storage to the referenced encoder's storage.
|
||||
deinit {
|
||||
let value: Any
|
||||
switch self.storage.count {
|
||||
case 0: value = NSDictionary()
|
||||
case 1: value = self.storage.popContainer()
|
||||
default: fatalError("Referencing encoder deallocated with multiple containers on stack.")
|
||||
}
|
||||
|
||||
switch self.reference {
|
||||
case .array(let array, let index):
|
||||
array.insert(value, at: index)
|
||||
|
||||
case .dictionary(let dictionary, let key):
|
||||
dictionary[NSString(string: key)] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
//
|
||||
// This source file is largely a copy of code from Swift.org open source project's
|
||||
// files JSONEncoder.swift and Codeable.swift.
|
||||
//
|
||||
// Unfortunately those files do not expose the internal _JSONEncoder and
|
||||
// _JSONDecoder classes, which are in fact dictionary encoder/decoders and
|
||||
// precisely what we want...
|
||||
//
|
||||
// The original code is copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors
|
||||
// Licensed under Apache License v2.0 with Runtime Library Exception
|
||||
//
|
||||
// See https://swift.org/LICENSE.txt for license information
|
||||
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
|
||||
//
|
||||
// Modifications and additional code here is copyright (c) 2018 Sam Deane, and
|
||||
// is licensed under the same terms.
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
import Foundation
|
||||
|
||||
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Error Utilities
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
internal extension EncodingError {
|
||||
/// Returns a `.invalidValue` error describing the given invalid floating-point value.
|
||||
///
|
||||
///
|
||||
/// - parameter value: The value that was invalid to encode.
|
||||
/// - parameter path: The path of `CodingKey`s taken to encode this value.
|
||||
/// - returns: An `EncodingError` with the appropriate path and debug description.
|
||||
internal static func _invalidFloatingPointValue<T : FloatingPoint>(_ value: T, at codingPath: [CodingKey]) -> EncodingError {
|
||||
let valueDescription: String
|
||||
if value == T.infinity {
|
||||
valueDescription = "\(T.self).infinity"
|
||||
} else if value == -T.infinity {
|
||||
valueDescription = "-\(T.self).infinity"
|
||||
} else {
|
||||
valueDescription = "\(T.self).nan"
|
||||
}
|
||||
|
||||
let debugDescription = "Unable to encode \(valueDescription) directly in Dictionary. Use DictionaryEncoder.NonConformingFloatEncodingStrategy.convertToString to specify how the value should be encoded."
|
||||
return .invalidValue(value, EncodingError.Context(codingPath: codingPath, debugDescription: debugDescription))
|
||||
}
|
||||
}
|
||||
|
||||
internal extension DecodingError {
|
||||
/// Returns a `.typeMismatch` error describing the expected type.
|
||||
///
|
||||
/// - parameter path: The path of `CodingKey`s taken to decode a value of this type.
|
||||
/// - parameter expectation: The type expected to be encountered.
|
||||
/// - parameter reality: The value that was encountered instead of the expected type.
|
||||
/// - returns: A `DecodingError` with the appropriate path and debug description.
|
||||
internal static func _typeMismatch(at path: [CodingKey], expectation: Any.Type, reality: Any) -> DecodingError {
|
||||
let description = "Expected to decode \(expectation) but found \(_typeDescription(of: reality)) instead."
|
||||
return .typeMismatch(expectation, Context(codingPath: path, debugDescription: description))
|
||||
}
|
||||
|
||||
/// Returns a description of the type of `value` appropriate for an error message.
|
||||
///
|
||||
/// - parameter value: The value whose type to describe.
|
||||
/// - returns: A string describing `value`.
|
||||
/// - precondition: `value` is one of the types below.
|
||||
internal static func _typeDescription(of value: Any) -> String {
|
||||
if value is NSNull {
|
||||
return "a null value"
|
||||
} else if value is NSNumber /* FIXM: If swift-corelibs-foundation isn't updated to use NSNumber, this check will be necessary: || value is Int || value is Double */ {
|
||||
return "a number"
|
||||
} else if value is String {
|
||||
return "a string/data"
|
||||
} else if value is [Any] {
|
||||
return "an array"
|
||||
} else if value is [String : Any] {
|
||||
return "a dictionary"
|
||||
} else {
|
||||
return "\(type(of: value))"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
import UIKit
|
||||
|
||||
open class CollectionDelegate: NSObject, UICollectionViewDelegate {
|
||||
|
||||
weak var scrollViewDelegate: UIScrollViewDelegate?
|
||||
|
||||
public weak var collectionView: UICollectionView? {
|
||||
didSet {
|
||||
collectionView?.delegate = self
|
||||
}
|
||||
}
|
||||
|
||||
public let collectionDriver: CollectionDriver
|
||||
|
||||
public init(driver: CollectionDriver) {
|
||||
self.collectionDriver = driver
|
||||
super.init()
|
||||
}
|
||||
|
||||
public func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
|
||||
guard !collectionDriver.isShowingSkeleton else { return }
|
||||
collectionDriver.item(at: indexPath).willDisplay(cell)
|
||||
}
|
||||
|
||||
public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||
guard let cell = collectionView.cellForItem(at: indexPath) else { return }
|
||||
collectionDriver.item(at: indexPath).didSelect(cell)
|
||||
}
|
||||
|
||||
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
scrollViewDelegate?.scrollViewDidScroll?(scrollView)
|
||||
|
||||
for section in 0..<collectionDriver.sections.count {
|
||||
let indexPath = IndexPath(item: 0, section: section)
|
||||
collectionDriver.supplement(ofKind: UICollectionElementKindSectionHeader, at: indexPath)?.didScroll(scrollView)
|
||||
collectionDriver.supplement(ofKind: UICollectionElementKindSectionFooter, at: indexPath)?.didScroll(scrollView)
|
||||
}
|
||||
}
|
||||
|
||||
public func scrollViewDidZoom(_ scrollView: UIScrollView) {
|
||||
scrollViewDelegate?.scrollViewDidZoom?(scrollView)
|
||||
}
|
||||
|
||||
public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
||||
scrollViewDelegate?.scrollViewWillBeginDragging?(scrollView)
|
||||
}
|
||||
|
||||
public func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
|
||||
scrollViewDelegate?.scrollViewWillEndDragging?(scrollView, withVelocity: velocity, targetContentOffset: targetContentOffset)
|
||||
}
|
||||
|
||||
public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
|
||||
scrollViewDelegate?.scrollViewDidEndDragging?(scrollView, willDecelerate: decelerate)
|
||||
}
|
||||
|
||||
public func scrollViewWillBeginDecelerating(_ scrollView: UIScrollView) {
|
||||
scrollViewDelegate?.scrollViewWillBeginDecelerating?(scrollView)
|
||||
}
|
||||
|
||||
public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
||||
scrollViewDelegate?.scrollViewDidEndDecelerating?(scrollView)
|
||||
}
|
||||
|
||||
public func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
|
||||
scrollViewDelegate?.scrollViewDidEndScrollingAnimation?(scrollView)
|
||||
}
|
||||
|
||||
public func viewForZooming(in scrollView: UIScrollView) -> UIView? {
|
||||
return scrollViewDelegate?.viewForZooming?(in: scrollView)
|
||||
}
|
||||
|
||||
public func scrollViewWillBeginZooming(_ scrollView: UIScrollView, with view: UIView?) {
|
||||
scrollViewDelegate?.scrollViewWillBeginZooming?(scrollView, with: view)
|
||||
}
|
||||
|
||||
public func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) {
|
||||
scrollViewDelegate?.scrollViewDidEndZooming?(scrollView, with: view, atScale: scale)
|
||||
}
|
||||
|
||||
public func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool {
|
||||
return scrollViewDelegate?.scrollViewShouldScrollToTop?(scrollView) ?? true
|
||||
}
|
||||
|
||||
public func scrollViewDidScrollToTop(_ scrollView: UIScrollView) {
|
||||
scrollViewDelegate?.scrollViewDidScrollToTop?(scrollView)
|
||||
}
|
||||
}
|
||||
@@ -1,348 +0,0 @@
|
||||
import UIKit
|
||||
import SkeletonView
|
||||
|
||||
public class CollectionDriver {
|
||||
public private(set) var mode = DisplayMode.normal
|
||||
private var diffMode = DiffMode.immediate
|
||||
|
||||
var willPerformBatchUpdate: ((UICollectionView) -> Void)? = nil
|
||||
var didPerformBatchUpdate: ((UICollectionView) -> Void)? = nil
|
||||
|
||||
public var collectionView: UICollectionView? {
|
||||
didSet {
|
||||
sections.attachTo(collectionView, driver: self)
|
||||
|
||||
if let collectionView = collectionView {
|
||||
collectionView.dataSource = wrapper
|
||||
collectionView.prefetchDataSource = wrapper
|
||||
collectionView.reloadData()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public var sections: [CollectionSection] {
|
||||
didSet {
|
||||
oldValue.attachTo(nil)
|
||||
sections.attachTo(collectionView, driver: self)
|
||||
|
||||
enqueueDiff {
|
||||
let changes = diff(old: oldValue, new: self.sections)
|
||||
return .sections(changes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public var emptyView: UIView
|
||||
|
||||
var isShowingSkeleton: Bool {
|
||||
guard let collectionView = collectionView else { return false }
|
||||
return !(collectionView.dataSource is CollectionDataSourceWrapper)
|
||||
}
|
||||
|
||||
private lazy var wrapper = CollectionDataSourceWrapper(collectionDriver: self)
|
||||
|
||||
// MARK: -
|
||||
|
||||
public init(sections: [CollectionSection] = [], emptyView: UIView = EmptyView.noData) {
|
||||
self.sections = sections
|
||||
self.emptyView = emptyView
|
||||
}
|
||||
|
||||
// MARK: - Modes
|
||||
|
||||
public func setLoadingMode(_ loadingMode: DisplayMode.LoadingMode) {
|
||||
self.mode = .loading(loadingMode)
|
||||
guard let collectionView = collectionView else { return }
|
||||
|
||||
switch loadingMode {
|
||||
case .refreshControl:
|
||||
if let refreshControl = collectionView.refreshControl, !refreshControl.isRefreshing {
|
||||
refreshControl.beginRefreshing()
|
||||
}
|
||||
case .skeleton:
|
||||
hideError()
|
||||
collectionView.showSkeleton(usingColor: UIColor.unselected)
|
||||
}
|
||||
}
|
||||
|
||||
public func refreshSkeleton() {
|
||||
guard let collectionView = collectionView else { return }
|
||||
collectionView.hideSkeleton()
|
||||
collectionView.alpha = 0
|
||||
delay(TimeInterval.oneFrame) {
|
||||
collectionView.showSkeleton(usingColor: UIColor.unselected)
|
||||
UIView.animate(withDuration: 0.2) {
|
||||
collectionView.alpha = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func setNormalMode(_ closure: @escaping () -> Void = {}) {
|
||||
let oldMode = mode
|
||||
mode = .normal
|
||||
|
||||
guard let collectionView = collectionView else { return }
|
||||
|
||||
hideError()
|
||||
|
||||
switch oldMode {
|
||||
case .loading(.skeleton):
|
||||
collectionView.hideSkeleton(reloadDataAfter: false)
|
||||
|
||||
// Disable diffing to avoid crashes and stuff
|
||||
diffMode = .disabled
|
||||
|
||||
closure()
|
||||
collectionView.reloadData()
|
||||
|
||||
DispatchQueue.main.async {
|
||||
// Reload data completed so it's safe to enable diff again
|
||||
self.diffMode = .immediate
|
||||
}
|
||||
|
||||
case .loading(.refreshControl):
|
||||
collectionView.refreshControl?.endRefreshing()
|
||||
fallthrough
|
||||
|
||||
case .error, .normal:
|
||||
DispatchQueue.main.async {
|
||||
closure()
|
||||
}
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
collectionView.backgroundView = self.isEmpty ? self.emptyView : nil
|
||||
}
|
||||
}
|
||||
|
||||
public func setErrorMode(_ error: Error) {
|
||||
let oldMode = mode
|
||||
mode = .error(error)
|
||||
|
||||
guard let collectionView = collectionView else { return }
|
||||
|
||||
switch oldMode {
|
||||
case .loading(.skeleton):
|
||||
collectionView.hideSkeleton(reloadDataAfter: false)
|
||||
|
||||
case .loading(.refreshControl):
|
||||
collectionView.refreshControl?.endRefreshing()
|
||||
|
||||
case .error, .normal:
|
||||
break
|
||||
}
|
||||
|
||||
if let errorView = collectionView.backgroundView as? ErrorView {
|
||||
errorView.error = error
|
||||
} else {
|
||||
let errorView = ErrorView()
|
||||
errorView.error = error
|
||||
collectionView.backgroundView = errorView
|
||||
}
|
||||
}
|
||||
|
||||
private func hideError() {
|
||||
collectionView?.backgroundView = nil
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Diff
|
||||
|
||||
public func batchUpdate(_ closure: () -> Void) {
|
||||
diffMode = .enqueue
|
||||
closure()
|
||||
|
||||
if let collectionView = collectionView {
|
||||
willPerformBatchUpdate?(collectionView)
|
||||
collectionView.performBatchUpdates({
|
||||
diffQueue.forEach { applyDiffResult($0) }
|
||||
diffQueue = []
|
||||
}, completion: { [weak self] _ in
|
||||
guard let `self` = self, let collectionView = self.collectionView else { return }
|
||||
self.didPerformBatchUpdate?(collectionView)
|
||||
})
|
||||
}
|
||||
|
||||
diffMode = .immediate
|
||||
}
|
||||
|
||||
enum DiffResult {
|
||||
case items([Change<CollectionItem>], section: Int)
|
||||
case sections([Change<CollectionSection>])
|
||||
}
|
||||
|
||||
private var diffQueue: [DiffResult] = []
|
||||
|
||||
func enqueueDiff(_ calculateDiff: @escaping () -> DiffResult) {
|
||||
guard let collectionView = collectionView else { return }
|
||||
|
||||
switch diffMode {
|
||||
case .immediate:
|
||||
willPerformBatchUpdate?(collectionView)
|
||||
collectionView.performBatchUpdates({
|
||||
applyDiffResult( calculateDiff() )
|
||||
}, completion: { [weak self] _ in
|
||||
guard let `self` = self, let collectionView = self.collectionView else { return }
|
||||
self.didPerformBatchUpdate?(collectionView)
|
||||
})
|
||||
case .enqueue:
|
||||
diffQueue.append( calculateDiff() )
|
||||
|
||||
case .disabled:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private func applyDiffResult(_ result: DiffResult) {
|
||||
guard let collectionView = collectionView else { return }
|
||||
switch result {
|
||||
case let .items(changes, section: sectionIndex):
|
||||
collectionView.reload(changes: changes, section: sectionIndex, calledInsideBatch: true)
|
||||
|
||||
case let .sections(changes):
|
||||
collectionView.reloadSections(changes: changes, calledInsideBatch: true)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Data
|
||||
|
||||
public var isEmpty: Bool {
|
||||
return sections.reduce(0, { $0 + $1.items.count }) == 0
|
||||
}
|
||||
|
||||
public func item(at indexPath: IndexPath) -> CollectionItem {
|
||||
return sections[indexPath.section].items[indexPath.item]
|
||||
}
|
||||
|
||||
public func safeItem(at indexPath: IndexPath) -> CollectionItem? {
|
||||
guard indexPath.section < sections.endIndex,
|
||||
indexPath.item < sections[indexPath.section].items.endIndex
|
||||
else { return nil }
|
||||
return sections[indexPath.section].items[indexPath.item]
|
||||
}
|
||||
|
||||
public func supplement(ofKind: String, at indexPath: IndexPath) -> CollectionSupplement? {
|
||||
return sections[indexPath.section].supplementaryItems[ofKind]?[indexPath.item]
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Mode Definition
|
||||
|
||||
extension CollectionDriver {
|
||||
public enum DisplayMode {
|
||||
case normal
|
||||
case loading(LoadingMode)
|
||||
case error(Error)
|
||||
|
||||
public enum LoadingMode {
|
||||
case refreshControl
|
||||
case skeleton
|
||||
}
|
||||
}
|
||||
|
||||
enum DiffMode {
|
||||
case immediate
|
||||
case enqueue
|
||||
case disabled
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Wrapper
|
||||
|
||||
private final class CollectionDataSourceWrapper: NSObject, SkeletonCollectionViewDataSource, UICollectionViewDataSourcePrefetching {
|
||||
|
||||
unowned let collectionDriver: CollectionDriver
|
||||
|
||||
init(collectionDriver: CollectionDriver) {
|
||||
self.collectionDriver = collectionDriver
|
||||
}
|
||||
|
||||
|
||||
// MARK: Data Source
|
||||
|
||||
func numberOfSections(in collectionView: UICollectionView) -> Int {
|
||||
return collectionDriver.sections.count
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||
return collectionDriver.sections[section].items.count
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||
let cell = collectionDriver.item(at: indexPath).dequeueCell()
|
||||
return cell
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
|
||||
if let supplement = collectionDriver.supplement(ofKind: kind, at: indexPath) {
|
||||
return supplement.dequeueView()
|
||||
} else {
|
||||
return UICollectionReusableView()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: Skeleton
|
||||
|
||||
func numSections(in collectionSkeletonView: UICollectionView) -> Int {
|
||||
return collectionDriver.sections.count
|
||||
}
|
||||
|
||||
func collectionSkeletonView(_ skeletonView: UICollectionView, cellIdentifierForItemAt indexPath: IndexPath) -> ReusableCellIdentifier {
|
||||
return collectionDriver.sections[indexPath.section].skeletonCellIdentifier.require(hint: "Specify skeleton cell")
|
||||
}
|
||||
|
||||
func collectionSkeletonView(_ skeletonView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||
if let count = collectionDriver.sections[section].skeletonCellsCount {
|
||||
return count
|
||||
} else if let collection = collectionDriver.collectionView,
|
||||
let flowlayout = collection.collectionViewLayout as? UICollectionViewFlowLayout {
|
||||
let count = Int(ceil(collection.frame.height/flowlayout.itemSize.height))
|
||||
return count
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: Prefetch
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) {
|
||||
guard !collectionDriver.isShowingSkeleton else { return }
|
||||
indexPaths.forEach {
|
||||
collectionDriver
|
||||
.safeItem(at: $0)? // indexPaths contains non-existing items if updating
|
||||
.prefetch()
|
||||
}
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, cancelPrefetchingForItemsAt indexPaths: [IndexPath]) {
|
||||
guard !collectionDriver.isShowingSkeleton else { return }
|
||||
indexPaths.forEach {
|
||||
collectionDriver
|
||||
.safeItem(at: $0)?
|
||||
.cancelPrefetch()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Private extensions
|
||||
|
||||
private extension Array where Element: CollectionSection {
|
||||
func attachTo(_ collectionView: UICollectionView?, driver: CollectionDriver? = nil) {
|
||||
if let collectionView = collectionView {
|
||||
enumerated().forEach {
|
||||
$1.attach(to: collectionView, at: $0)
|
||||
$1.driver = driver
|
||||
}
|
||||
} else {
|
||||
forEach { $0.detach() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,159 +0,0 @@
|
||||
import UIKit
|
||||
|
||||
public class CollectionFlowDelegate: CollectionDelegate, UICollectionViewDelegateFlowLayout {
|
||||
|
||||
public var defaultLayout: Layout
|
||||
private var layouts: [CollectionSection: Layout] = [:]
|
||||
|
||||
var flowLayout: UICollectionViewFlowLayout {
|
||||
guard let layout = collectionView?.collectionViewLayout as? UICollectionViewFlowLayout else {
|
||||
fatalError("CollectionFlowDelegate can manage only collections with flow layout")
|
||||
}
|
||||
return layout
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
public init(driver: CollectionDriver, defaultLayout: Layout = Layout()) {
|
||||
self.defaultLayout = defaultLayout
|
||||
super.init(driver: driver)
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Section Layouts
|
||||
|
||||
public func layoutForSection(_ section: CollectionSection) -> Layout {
|
||||
if let layout = layouts[section] {
|
||||
return layout
|
||||
}
|
||||
|
||||
layouts[section] = defaultLayout
|
||||
return defaultLayout
|
||||
}
|
||||
|
||||
func layoutForSection(_ index: Int) -> Layout {
|
||||
return layoutForSection(collectionDriver.sections[index])
|
||||
}
|
||||
|
||||
public func configureLayout(for section: CollectionSection, closure: (inout Layout) -> Void) {
|
||||
layouts[section] = layoutForSection(section).with(closure)
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Delegate
|
||||
|
||||
public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
|
||||
|
||||
let layout = layoutForSection(indexPath.section)
|
||||
let constraint = layout.itemConstraints(layout: flowLayout)
|
||||
if collectionDriver.isShowingSkeleton {
|
||||
if let itemSizeFor = collectionDriver.sections[indexPath.section].skeletonSizeForConstraint {
|
||||
let itemSize = itemSizeFor(constraint)
|
||||
return itemSize
|
||||
} else {
|
||||
return CGSize.zero
|
||||
}
|
||||
} else {
|
||||
let itemSize = collectionDriver.item(at: indexPath).sizeForConstraint(constraint)
|
||||
return itemSize
|
||||
}
|
||||
}
|
||||
|
||||
public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
|
||||
return layoutForSection(section).lineSpacing
|
||||
}
|
||||
|
||||
public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
|
||||
return layoutForSection(section).interitemSpacing
|
||||
}
|
||||
|
||||
public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
|
||||
return layoutForSection(section).insets
|
||||
}
|
||||
|
||||
public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
|
||||
|
||||
let constraint = layoutForSection(section).supplementConstraints(layout: flowLayout)
|
||||
return collectionDriver.sections[section].header?.sizeForConstraint(constraint) ?? .zero
|
||||
}
|
||||
|
||||
public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize {
|
||||
|
||||
let constraint = layoutForSection(section).supplementConstraints(layout: flowLayout)
|
||||
return collectionDriver.sections[section].footer?.sizeForConstraint(constraint) ?? .zero
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Layout
|
||||
|
||||
extension CollectionFlowDelegate {
|
||||
|
||||
public struct Layout: Then {
|
||||
public var itemsPerRow: Int = 1
|
||||
public var lineSpacing: CGFloat = 0
|
||||
public var interitemSpacing: CGFloat = 0
|
||||
public var insets: UIEdgeInsets = .zero
|
||||
|
||||
public init() {}
|
||||
|
||||
func supplementConstraints(layout: UICollectionViewFlowLayout) -> CGSize {
|
||||
switch layout.scrollDirection {
|
||||
case .vertical: return CGSize(layout.collectionViewWidth, 0)
|
||||
case .horizontal: return CGSize(0, layout.collectionViewHeight)
|
||||
}
|
||||
}
|
||||
|
||||
func itemConstraints(layout: UICollectionViewFlowLayout) -> CGSize {
|
||||
switch layout.scrollDirection {
|
||||
case .vertical:
|
||||
let width = (layout.collectionViewWidth - (
|
||||
CGFloat(itemsPerRow - 1) * interitemSpacing
|
||||
+ insets.left
|
||||
+ insets.right
|
||||
)) / CGFloat(itemsPerRow)
|
||||
return CGSize(width, 0)
|
||||
|
||||
case .horizontal:
|
||||
let height = (layout.collectionViewHeight - (
|
||||
CGFloat(itemsPerRow - 1) * interitemSpacing
|
||||
+ insets.top
|
||||
+ insets.bottom
|
||||
)) / CGFloat(itemsPerRow)
|
||||
return CGSize(0, height)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: -
|
||||
|
||||
public extension CollectionSection {
|
||||
public var header: CollectionSupplement? {
|
||||
get {
|
||||
return supplementaryItems[UICollectionElementKindSectionHeader]?.first
|
||||
}
|
||||
set {
|
||||
supplementaryItems[UICollectionElementKindSectionHeader]
|
||||
= newValue == nil ? [] : [newValue!]
|
||||
}
|
||||
}
|
||||
|
||||
public var footer: CollectionSupplement? {
|
||||
get {
|
||||
return supplementaryItems[UICollectionElementKindSectionFooter]?.first
|
||||
}
|
||||
set {
|
||||
supplementaryItems[UICollectionElementKindSectionFooter]
|
||||
= newValue == nil ? [] : [newValue!]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension UICollectionViewFlowLayout {
|
||||
var collectionViewWidth: CGFloat { return collectionView?.bounds.width ?? 0 }
|
||||
var collectionViewHeight: CGFloat { return collectionView?.bounds.height ?? 0 }
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
import UIKit
|
||||
|
||||
public class CollectionItem: Hashable {
|
||||
|
||||
private(set) public var data: AnyHashable
|
||||
private let cellClass: AnyClass
|
||||
var indexPath: IndexPath?
|
||||
weak var collectionView: UICollectionView?
|
||||
|
||||
|
||||
// MARK: - Blocks
|
||||
|
||||
var dequeueCell: () -> UICollectionViewCell = { fatalError() }
|
||||
var fillCell: () -> Void = {}
|
||||
let prefetch: () -> Void
|
||||
let cancelPrefetch: () -> Void
|
||||
let willDisplay: (UICollectionViewCell) -> Void
|
||||
let didSelect: (UICollectionViewCell) -> Void
|
||||
var sizeForConstraint: (CGSize) -> CGSize = { _ in fatalError() }
|
||||
|
||||
public init<T: CollectionItemProtocol>(_ controller: T) {
|
||||
cellClass = T.Cell.self
|
||||
data = AnyHashable(controller.data)
|
||||
|
||||
willDisplay = {
|
||||
if let cell = $0 as? T.Cell {
|
||||
controller.cellWillDisplay(cell)
|
||||
}
|
||||
}
|
||||
|
||||
didSelect = {
|
||||
if let cell = $0 as? T.Cell {
|
||||
controller.cellSelected(cell)
|
||||
}
|
||||
}
|
||||
|
||||
prefetch = controller.prefetchContent
|
||||
cancelPrefetch = controller.cancelPrefetchContent
|
||||
|
||||
dequeueCell = { [unowned self] in
|
||||
guard let indexPath = self.indexPath,
|
||||
let collectionView = self.collectionView
|
||||
else { fatalError() }
|
||||
|
||||
let cell: T.Cell = collectionView.dequeueCell(for: indexPath)
|
||||
controller.fillCell(cell, animated: false)
|
||||
return cell
|
||||
}
|
||||
|
||||
fillCell = { [weak self] in
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let `self` = self, let indexPath = self.indexPath, let collectionView = self.collectionView else { return }
|
||||
if let cell = collectionView.cellForItem(at: indexPath) as? T.Cell {
|
||||
controller.fillCell(cell, animated: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sizeForConstraint = {
|
||||
return controller.size(constrainedTo: $0.width, height: $0.height)
|
||||
}
|
||||
|
||||
controller.events.dataUpdated = { [unowned self] newData in
|
||||
self.data = newData
|
||||
self.fillCell()
|
||||
}
|
||||
|
||||
controller.events.dataAndSizeUpdated = { [unowned self] newData in
|
||||
self.data = newData
|
||||
guard let indexPath = self.indexPath,
|
||||
let collectionView = self.collectionView
|
||||
else { return }
|
||||
UIView.performWithoutAnimation {
|
||||
collectionView.reloadItems(at: [ indexPath ])
|
||||
}
|
||||
}
|
||||
|
||||
controller.events.animatedSizeUpdated = { [unowned self] in
|
||||
self.collectionView?.performBatchUpdates({}, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Hashable
|
||||
|
||||
public var hashValue: Int {
|
||||
return data.hashValue
|
||||
}
|
||||
|
||||
public static func == (lhs: CollectionItem, rhs: CollectionItem) -> Bool {
|
||||
return lhs.cellClass == rhs.cellClass && lhs.data == rhs.data
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
import UIKit
|
||||
|
||||
public struct CollectionItemEvents<Data> {
|
||||
public var dataUpdated: (Data) -> Void = { _ in }
|
||||
public var dataAndSizeUpdated: (Data) -> Void = { _ in }
|
||||
public var animatedSizeUpdated: () -> Void = { }
|
||||
public init() { }
|
||||
}
|
||||
|
||||
public protocol CollectionItemProtocol: class {
|
||||
associatedtype Cell: UICollectionViewCell
|
||||
associatedtype Data: Hashable
|
||||
|
||||
// Data-related
|
||||
var data: Data { get }
|
||||
|
||||
// Cell-related
|
||||
func fillCell(_ cell: Cell, animated: Bool)
|
||||
func size(constrainedTo width: CGFloat, height: CGFloat) -> CGSize
|
||||
func cellWillDisplay(_ cell: Cell)
|
||||
func cellSelected(_ cell: Cell)
|
||||
|
||||
// Events to pass to collection driver
|
||||
var events: CollectionItemEvents<Data> { get set }
|
||||
|
||||
// Prefetching
|
||||
func prefetchContent()
|
||||
func cancelPrefetchContent()
|
||||
}
|
||||
|
||||
public extension CollectionItemProtocol {
|
||||
static func registerReusableCell(in collectionView: UICollectionView) {
|
||||
collectionView.registerCell(Cell.self)
|
||||
}
|
||||
|
||||
var collectionItem: CollectionItem {
|
||||
return CollectionItem(self)
|
||||
}
|
||||
|
||||
func singleItemSection(_ identifier: String = ProcessInfo.processInfo.globallyUniqueString) -> CollectionSection {
|
||||
return CollectionSection(identifier: identifier, items: [ self.collectionItem ])
|
||||
}
|
||||
|
||||
// MARK: - Default implementations
|
||||
|
||||
func prefetchContent() { }
|
||||
func cancelPrefetchContent() { }
|
||||
func cellWillDisplay(_ cell: Cell) { }
|
||||
func cellSelected(_ cell: Cell) { }
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import UIKit
|
||||
import VendefyService
|
||||
|
||||
final class SingleCellController<CollectionCell: UICollectionViewCell & Fillable, ItemData>: CollectionItemProtocol where CollectionCell.Data == ItemData {
|
||||
|
||||
typealias Cell = CollectionCell
|
||||
typealias Data = ItemData
|
||||
|
||||
var cellSelected: ((Cell, Data) -> ())?
|
||||
var cellConfigure: ((Cell) -> ())?
|
||||
|
||||
var data: ItemData
|
||||
var events = CollectionItemEvents<Data>()
|
||||
|
||||
let height: CGFloat
|
||||
|
||||
init(data: ItemData, height: CGFloat) {
|
||||
self.data = data
|
||||
self.height = height
|
||||
}
|
||||
|
||||
// MARK: CollectionItemProtocol
|
||||
func fillCell(_ cell: CollectionCell, animated: Bool) {
|
||||
cell.fill(data)
|
||||
cellConfigure?(cell)
|
||||
}
|
||||
|
||||
func size(constrainedTo width: CGFloat, height: CGFloat) -> CGSize {
|
||||
return CGSize(width: width, height: self.height)
|
||||
}
|
||||
|
||||
func cellSelected(_ cell: Cell) {
|
||||
cellSelected?(cell, data)
|
||||
}
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
import UIKit
|
||||
import SkeletonView
|
||||
|
||||
public class CollectionSection: Hashable, Then {
|
||||
|
||||
private var sectionIndex: Int?
|
||||
private weak var collectionView: UICollectionView?
|
||||
weak var driver: CollectionDriver?
|
||||
|
||||
|
||||
// MARK: - Data
|
||||
|
||||
public var items: [CollectionItem] {
|
||||
didSet {
|
||||
oldValue.attachTo(nil, sectionIndex: nil)
|
||||
items.attachTo(collectionView, sectionIndex: sectionIndex)
|
||||
|
||||
if let collectionView = collectionView,
|
||||
let sectionIndex = sectionIndex,
|
||||
let driver = driver
|
||||
{
|
||||
driver.enqueueDiff {
|
||||
// Call fill for visible cells so bindings are set because
|
||||
// CollectionItem are always recreated.
|
||||
collectionView.indexPathsForVisibleItems.forEach { indexPath in
|
||||
guard indexPath.section == sectionIndex else { return }
|
||||
|
||||
if indexPath.item < self.items.endIndex {
|
||||
self.items[indexPath.item].fillCell()
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate
|
||||
let changes = diff(old: oldValue, new: self.items)
|
||||
return .items(changes, section: sectionIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public var supplementaryItems: [String: [CollectionSupplement]] = [:] {
|
||||
didSet {
|
||||
supplementaryItems.values.forEach {
|
||||
$0.attachTo(collectionView, sectionIndex: sectionIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public let identifier: String
|
||||
|
||||
|
||||
// MARK: - Skeleton
|
||||
|
||||
public var skeletonCellIdentifier: ReusableCellIdentifier?
|
||||
public var skeletonSizeForConstraint: ((CGSize) -> CGSize)?
|
||||
public var skeletonCellsCount: Int?
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
public init(identifier: String = ProcessInfo.processInfo.globallyUniqueString, items: [CollectionItem] = []) {
|
||||
self.identifier = identifier
|
||||
self.items = items
|
||||
}
|
||||
|
||||
|
||||
// MARK: -
|
||||
|
||||
func attach(to collectionView: UICollectionView, at index: Int) {
|
||||
self.collectionView = collectionView
|
||||
self.sectionIndex = index
|
||||
items.attachTo(collectionView, sectionIndex: index)
|
||||
supplementaryItems.values.forEach {
|
||||
$0.attachTo(collectionView, sectionIndex: index)
|
||||
}
|
||||
}
|
||||
|
||||
func detach() {
|
||||
collectionView = nil
|
||||
sectionIndex = nil
|
||||
items.attachTo(nil, sectionIndex: nil)
|
||||
supplementaryItems.values.forEach {
|
||||
$0.attachTo(nil, sectionIndex: nil)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Hashable
|
||||
|
||||
public var hashValue: Int {
|
||||
return identifier.hashValue
|
||||
}
|
||||
|
||||
public static func == (lhs: CollectionSection, rhs: CollectionSection) -> Bool {
|
||||
return lhs.identifier == rhs.identifier && lhs.items == rhs.items
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: -
|
||||
|
||||
private extension Array where Element: CollectionItem {
|
||||
func attachTo(_ collectionView: UICollectionView?, sectionIndex: Int?) {
|
||||
enumerated().forEach { index, item in
|
||||
item.collectionView = collectionView
|
||||
item.indexPath = sectionIndex.map { IndexPath(item: index, section: $0) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension Array where Element: CollectionSupplement {
|
||||
func attachTo(_ collectionView: UICollectionView?, sectionIndex: Int?) {
|
||||
enumerated().forEach { index, item in
|
||||
item.collectionView = collectionView
|
||||
item.indexPath = sectionIndex.map { IndexPath(item: index, section: $0) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import UIKit
|
||||
|
||||
public class CollectionSupplement {
|
||||
|
||||
var indexPath: IndexPath?
|
||||
weak var collectionView: UICollectionView?
|
||||
|
||||
// MARK: - Blocks
|
||||
|
||||
var sizeForConstraint: (CGSize) -> CGSize = { _ in fatalError() }
|
||||
var dequeueView: () -> UICollectionReusableView = { fatalError() }
|
||||
var didScroll: (UIScrollView) -> () = { _ in }
|
||||
|
||||
// MARK: -
|
||||
|
||||
public init<T: CollectionSupplementProtocol>(_ controller: T) {
|
||||
sizeForConstraint = {
|
||||
return controller.sizeForConstraint($0)
|
||||
}
|
||||
|
||||
dequeueView = { [unowned self] in
|
||||
guard let collectionView = self.collectionView,
|
||||
let indexPath = self.indexPath
|
||||
else { fatalError() }
|
||||
|
||||
let view: T.View = collectionView.dequeueSupplement(kind: T.kind, for: indexPath)
|
||||
controller.fill(view)
|
||||
return view
|
||||
}
|
||||
|
||||
didScroll = { scrollView in controller.didScroll(scrollView: scrollView) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
import UIKit
|
||||
|
||||
public protocol CollectionSupplementProtocol {
|
||||
associatedtype View: UICollectionReusableView
|
||||
static var kind: String { get }
|
||||
func sizeForConstraint(_ constraint: CGSize) -> CGSize
|
||||
func fill(_ view: View)
|
||||
func didScroll(scrollView: UIScrollView)
|
||||
}
|
||||
|
||||
public extension CollectionSupplementProtocol {
|
||||
static func registerReusable(in collectionView: UICollectionView) {
|
||||
collectionView.registerSupplement(View.self, kind: kind)
|
||||
}
|
||||
|
||||
var supplementaryItem: CollectionSupplement {
|
||||
return CollectionSupplement(self)
|
||||
}
|
||||
|
||||
// default implementation
|
||||
func didScroll(scrollView: UIScrollView) { }
|
||||
}
|
||||
@@ -2,8 +2,8 @@ import Foundation
|
||||
import UIKit
|
||||
|
||||
public enum Haptic {
|
||||
case impact(UIImpactFeedbackStyle)
|
||||
case notification(UINotificationFeedbackType)
|
||||
case impact(UIImpactFeedbackGenerator.FeedbackStyle)
|
||||
case notification(UINotificationFeedbackGenerator.FeedbackType)
|
||||
case selection
|
||||
|
||||
public func generate() {
|
||||
|
||||
@@ -29,7 +29,7 @@ extension Hapticable where Self: UIButton {
|
||||
set { setAssociatedObject(&hapticKey, newValue) }
|
||||
}
|
||||
|
||||
public var hapticControlEvents: UIControlEvents? {
|
||||
public var hapticControlEvents: UIControl.Event? {
|
||||
get { return getAssociatedObject(&eventKey) }
|
||||
set { setAssociatedObject(&eventKey, newValue) }
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
@@ -37,7 +37,7 @@ public extension SwiftyImageView {
|
||||
case flipFromTop(TimeInterval)
|
||||
case custom(
|
||||
duration: TimeInterval,
|
||||
animationOptions: UIViewAnimationOptions,
|
||||
animationOptions: UIView.AnimationOptions,
|
||||
animations: (SwiftyImageView, UIImage) -> Void,
|
||||
completion: ((Bool) -> Void)?
|
||||
)
|
||||
@@ -67,10 +67,10 @@ public extension SwiftyImageView {
|
||||
}
|
||||
|
||||
/// The animation options of the image transition.
|
||||
public var animationOptions: UIViewAnimationOptions {
|
||||
public var animationOptions: UIView.AnimationOptions {
|
||||
switch self {
|
||||
case .noTransition:
|
||||
return UIViewAnimationOptions()
|
||||
return UIView.AnimationOptions()
|
||||
case .crossDissolve:
|
||||
return .transitionCrossDissolve
|
||||
case .curlDown:
|
||||
@@ -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,72 +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.
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -84,7 +89,7 @@ public extension UIControl {
|
||||
static var SignalDictionaryKey = "signals_signalKey"
|
||||
}
|
||||
|
||||
private static let eventToKey: [UIControlEvents: NSString] = [
|
||||
private static let eventToKey: [UIControl.Event: NSString] = [
|
||||
.touchDown: "TouchDown",
|
||||
.touchDownRepeat: "TouchDownRepeat",
|
||||
.touchDragInside: "TouchDragInside",
|
||||
@@ -92,6 +97,7 @@ public extension UIControl {
|
||||
.touchDragEnter: "TouchDragEnter",
|
||||
.touchDragExit: "TouchDragExit",
|
||||
.touchUpInside: "TouchUpInside",
|
||||
.primaryActionTriggered: "PrimaryActionTriggered",
|
||||
.touchUpOutside: "TouchUpOutside",
|
||||
.touchCancel: "TouchCancel",
|
||||
.valueChanged: "ValueChanged",
|
||||
@@ -100,7 +106,7 @@ public extension UIControl {
|
||||
.editingDidEnd: "EditingDidEnd",
|
||||
.editingDidEndOnExit: "EditingDidEndOnExit"]
|
||||
|
||||
private func getOrCreateSignalForUIControlEvent(_ event: UIControlEvents) -> Signal<(Void)> {
|
||||
private func getOrCreateSignalForUIControlEvent(_ event: UIControl.Event) -> Signal<(Void)> {
|
||||
guard let key = UIControl.eventToKey[event] else {
|
||||
assertionFailure("Event type is not handled")
|
||||
return Signal()
|
||||
@@ -117,7 +123,7 @@ public extension UIControl {
|
||||
}
|
||||
}
|
||||
|
||||
private func handleUIControlEvent(_ uiControlEvent: UIControlEvents) {
|
||||
private func handleUIControlEvent(_ uiControlEvent: UIControl.Event) {
|
||||
getOrCreateSignalForUIControlEvent(uiControlEvent).fire(())
|
||||
}
|
||||
|
||||
@@ -148,6 +154,10 @@ public extension UIControl {
|
||||
@objc private dynamic func eventHandlerTouchUpInside() {
|
||||
handleUIControlEvent(.touchUpInside)
|
||||
}
|
||||
|
||||
@objc private dynamic func eventHandlerPrimaryActionTriggered() {
|
||||
handleUIControlEvent(.primaryActionTriggered)
|
||||
}
|
||||
|
||||
@objc private dynamic func eventHandlerTouchUpOutside() {
|
||||
handleUIControlEvent(.touchUpOutside)
|
||||
@@ -178,7 +188,7 @@ public extension UIControl {
|
||||
}
|
||||
}
|
||||
|
||||
extension UIControlEvents: Hashable {
|
||||
extension UIControl.Event: Hashable {
|
||||
public var hashValue: Int {
|
||||
return Int(self.rawValue)
|
||||
}
|
||||
|
||||
@@ -73,39 +73,14 @@ extension Collection where Self.Index == Self.Indices.Iterator.Element {
|
||||
}
|
||||
}
|
||||
|
||||
public extension Collection {
|
||||
|
||||
/**
|
||||
Creats a shuffled version of this array using the Fisher-Yates (fast and uniform) shuffle.
|
||||
- returns: A shuffled version of this array.
|
||||
*/
|
||||
public func shuffled() -> [Iterator.Element] {
|
||||
var list = Array(self)
|
||||
list.shuffle()
|
||||
return list
|
||||
}
|
||||
}
|
||||
|
||||
public extension MutableCollection where Index == Int {
|
||||
/**
|
||||
Shuffle the array using the Fisher-Yates (fast and uniform) shuffle. Mutating.
|
||||
*/
|
||||
public mutating func shuffle() {
|
||||
// Empty and single-element collections don't shuffle.
|
||||
guard count > 1 else { return }
|
||||
|
||||
for i in 0 ..< (count - 1) {
|
||||
let j = Int(arc4random_uniform(UInt32(count - i))) + i
|
||||
guard i != j else { continue }
|
||||
self.swapAt(i, j)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
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,9 +22,8 @@ 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
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,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
|
||||
}
|
||||
|
||||
@@ -41,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
|
||||
}
|
||||
}
|
||||
@@ -51,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
|
||||
}
|
||||
}
|
||||
@@ -69,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)
|
||||
}
|
||||
|
||||
@@ -82,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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ extension String {
|
||||
size = CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)
|
||||
}
|
||||
|
||||
let attributes: [NSAttributedStringKey: Any] = [ NSAttributedStringKey.font: font ]
|
||||
let attributes: [NSAttributedString.Key: Any] = [ NSAttributedString.Key.font: font ]
|
||||
let result = (self as NSString).boundingRect(with: size,options: [ .usesLineFragmentOrigin ], attributes: attributes, context: nil).size
|
||||
|
||||
return result
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
protocol InteractiveDismiss: class {
|
||||
|
||||
var canBeginInteractiveDismiss: Bool { get }
|
||||
var backgroundView: UIView { get }
|
||||
var contentView: UIView { get }
|
||||
|
||||
func setupInteractiveDismission()
|
||||
func viewDidCompleteInteractiveDismiss()
|
||||
func viewPossiblyWillDissmiss()
|
||||
}
|
||||
|
||||
private struct AssociatedKeys {
|
||||
static var exceededThreshold: Int = 0
|
||||
static var totalTranslation: Int = 1
|
||||
static var gestureDelegate: Int = 2
|
||||
}
|
||||
|
||||
extension InteractiveDismiss where Self: UIViewController {
|
||||
|
||||
private var exceededDismissionThreshold: Bool {
|
||||
get { return getAssociatedObject(&AssociatedKeys.exceededThreshold) ?? false }
|
||||
set { setAssociatedObject(&AssociatedKeys.exceededThreshold, newValue) }
|
||||
}
|
||||
|
||||
private var totalTranslationY: CGFloat {
|
||||
get { return getAssociatedObject(&AssociatedKeys.totalTranslation) ?? 0 }
|
||||
set { setAssociatedObject(&AssociatedKeys.totalTranslation, newValue) }
|
||||
}
|
||||
|
||||
private var gestureDelegate: GestureRecognizerDelegate? {
|
||||
get { return getAssociatedObject(&AssociatedKeys.gestureDelegate) }
|
||||
set { setAssociatedObject(&AssociatedKeys.gestureDelegate, newValue) }
|
||||
}
|
||||
|
||||
func dismissAnimated() {
|
||||
animateExit()
|
||||
}
|
||||
|
||||
func setupInteractiveDismission() {
|
||||
|
||||
let panGesture = UIPanGestureRecognizer {[weak self] recognizer in
|
||||
self?.didPanView(panGesture: recognizer as! UIPanGestureRecognizer)
|
||||
}
|
||||
panGesture.cancelsTouchesInView = false
|
||||
let delegate = GestureRecognizerDelegate()
|
||||
delegate.recognizerShouldBegin = { [unowned self] _ in return self.canBeginInteractiveDismiss }
|
||||
panGesture.delegate = delegate
|
||||
gestureDelegate = delegate
|
||||
view.addGestureRecognizer(panGesture)
|
||||
}
|
||||
|
||||
private func didPanView(panGesture: UIPanGestureRecognizer) {
|
||||
|
||||
// Update translation
|
||||
let translation = panGesture.translation(in: view)
|
||||
|
||||
// Check which view should update
|
||||
if contentView.frame.origin.y + translation.y > 0 {
|
||||
|
||||
// We are scrolling down
|
||||
totalTranslationY += translation.y
|
||||
|
||||
// Check threshold
|
||||
if totalTranslationY > Theme.scrollDownSlopThreshold {
|
||||
contentView.frame.origin.y += translation.y
|
||||
|
||||
// Updated the view behind this with the proper index
|
||||
// Prevents a jitter when exiting
|
||||
if !exceededDismissionThreshold {
|
||||
exceededDismissionThreshold = true
|
||||
viewPossiblyWillDissmiss()
|
||||
}
|
||||
} else {
|
||||
contentView.frame.origin.y = 0
|
||||
}
|
||||
|
||||
// Perform changes
|
||||
performTranslationBasedStyles()
|
||||
}
|
||||
|
||||
// Handle extras
|
||||
switch panGesture.state {
|
||||
case .ended:
|
||||
if totalTranslationY > Theme.scrollDownSlopThreshold {
|
||||
|
||||
// Determine velocity
|
||||
let velocity = panGesture.velocity(in: view)
|
||||
let projectedY = Inertia.project(initialVelocity: velocity.y)
|
||||
let duration = TimeInterval(view.frame.size.height / velocity.y)
|
||||
|
||||
// Handle case
|
||||
if projectedY >= Theme.flingToDismissVelocityThreshold {
|
||||
animateExit(duration, projectedY)
|
||||
} else if contentView.frame.origin.y >= view.frame.height * 0.4 {
|
||||
animateExit()
|
||||
} else {
|
||||
let springVelocity = projectedY / view.frame.height
|
||||
animateBack(intensity: springVelocity)
|
||||
}
|
||||
}
|
||||
|
||||
// Reset
|
||||
totalTranslationY = 0
|
||||
exceededDismissionThreshold = false
|
||||
default: break
|
||||
}
|
||||
|
||||
// Reset
|
||||
panGesture.setTranslation(.zero, in: view)
|
||||
}
|
||||
|
||||
private func animateExit(_ duration: TimeInterval = 0.33, _ projectedY: CGFloat = 0) {
|
||||
UIView.animate(withDuration: duration, animations: {
|
||||
self.contentView.frame.origin.y = self.view.frame.height + projectedY
|
||||
self.backgroundView.alpha = 0
|
||||
}) { _ in
|
||||
self.viewDidCompleteInteractiveDismiss()
|
||||
self.dismiss(animated: false, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
private func animateBack(intensity: CGFloat) {
|
||||
let velocity = intensity.limited(1, 1 + intensity)
|
||||
UIView.animate(withDuration: 0.33, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: velocity, options: .allowUserInteraction, animations: {
|
||||
self.contentView.frame.origin.y = 0
|
||||
self.backgroundView.alpha = 1
|
||||
}, completion: nil)
|
||||
}
|
||||
|
||||
// Changes styles based on the drag og the content view
|
||||
private func performTranslationBasedStyles() {
|
||||
let translation = 1 - (contentView.frame.origin.y / view.frame.height)
|
||||
backgroundView.alpha = translation
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Theme
|
||||
private enum Theme {
|
||||
static let scrollDownSlopThreshold: CGFloat = 36
|
||||
static let flingToDismissVelocityThreshold: CGFloat = 150
|
||||
}
|
||||
@@ -88,7 +88,7 @@ public final class ScaleModalDismissAC: NSObject, UIViewControllerAnimatedTransi
|
||||
|
||||
}, completion: { _ in
|
||||
fromVC.view.removeFromSuperview()
|
||||
fromVC.removeFromParentViewController()
|
||||
fromVC.removeFromParent()
|
||||
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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,74 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import UIKit.UIGestureRecognizerSubclass
|
||||
|
||||
|
||||
protocol TouchInteraction: class {
|
||||
func setupDefaultTouchInteraction()
|
||||
}
|
||||
|
||||
|
||||
extension UIView: TouchInteraction {}
|
||||
|
||||
|
||||
extension TouchInteraction where Self: UIView {
|
||||
|
||||
func setupDefaultTouchInteraction() {
|
||||
|
||||
let recognizer = TouchInteractionRecognizer { recognizer in
|
||||
switch recognizer.state {
|
||||
case .began:
|
||||
UIView.animate(withDuration: 0.1, delay: 0, options: [.beginFromCurrentState, .allowUserInteraction], animations: {
|
||||
self.transform = CGAffineTransform(scaleX: 0.9, y: 0.9)
|
||||
self.alpha = 0.6
|
||||
})
|
||||
case .ended, .failed, .cancelled:
|
||||
UIView.animate(withDuration: 0.175, delay: 0, options: [.beginFromCurrentState, .allowUserInteraction], animations: {
|
||||
self.transform = CGAffineTransform(scaleX: 1, y: 1)
|
||||
self.alpha = 1
|
||||
})
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
||||
self.addGestureRecognizer(recognizer)
|
||||
self.isUserInteractionEnabled = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class TouchInteractionRecognizer: UIGestureRecognizer {
|
||||
|
||||
override init(target: Any?, action: Selector?) {
|
||||
super.init(target: target, action: action)
|
||||
delaysTouchesBegan = false
|
||||
delaysTouchesEnded = false
|
||||
cancelsTouchesInView = false
|
||||
}
|
||||
|
||||
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
super.touchesBegan(touches, with: event)
|
||||
if touches.count != 1 {
|
||||
state = .failed
|
||||
}
|
||||
state = .began
|
||||
}
|
||||
|
||||
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
super.touchesEnded(touches, with: event)
|
||||
if touches.count != 1 {
|
||||
state = .failed
|
||||
}
|
||||
state = .ended
|
||||
}
|
||||
|
||||
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
super.touchesCancelled(touches, with: event)
|
||||
state = .cancelled
|
||||
}
|
||||
|
||||
override func reset() {
|
||||
super.reset()
|
||||
state = .possible
|
||||
}
|
||||
}
|
||||
@@ -25,25 +25,25 @@ extension UIBarButtonItem {
|
||||
get { return objc_getAssociatedObject(self, &AssociatedKeys.ActionName) as? Action }
|
||||
}
|
||||
|
||||
public convenience init(image: UIImage?, style: UIBarButtonItemStyle = .plain, action: @escaping () -> Void) {
|
||||
public convenience init(image: UIImage?, style: UIBarButtonItem.Style = .plain, action: @escaping () -> Void) {
|
||||
let handler = Action(action: action)
|
||||
self.init(image: image, style: style, target: handler, action: #selector(Action.handleAction(_:)))
|
||||
barButtonAction = handler
|
||||
}
|
||||
|
||||
public convenience init(title: String?, style: UIBarButtonItemStyle = .plain, action: @escaping () -> Void) {
|
||||
public convenience init(title: String?, style: UIBarButtonItem.Style = .plain, action: @escaping () -> Void) {
|
||||
let handler = Action(action: action)
|
||||
self.init(title: title, style: style, target: handler, action: #selector(Action.handleAction(_:)))
|
||||
barButtonAction = handler
|
||||
}
|
||||
|
||||
public convenience init(barButtonSystemItem: UIBarButtonSystemItem, action: @escaping () -> Void) {
|
||||
public convenience init(barButtonSystemItem: UIBarButtonItem.SystemItem, action: @escaping () -> Void) {
|
||||
let handler = Action(action: action)
|
||||
self.init(barButtonSystemItem: barButtonSystemItem, target: handler, action: #selector(Action.handleAction(_:)))
|
||||
barButtonAction = handler
|
||||
}
|
||||
|
||||
public convenience init(image: UIImage?, landscapeImagePhone: UIImage?, style: UIBarButtonItemStyle, action: @escaping () -> Void) {
|
||||
public convenience init(image: UIImage?, landscapeImagePhone: UIImage?, style: UIBarButtonItem.Style, action: @escaping () -> Void) {
|
||||
let handler = Action(action: action)
|
||||
self.init(image: image, landscapeImagePhone: landscapeImagePhone, style: style, target: handler, action: #selector(Action.handleAction(_:)))
|
||||
barButtonAction = handler
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
public final class HorizontalAlignmentLayout: 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 left
|
||||
case center
|
||||
case right
|
||||
}
|
||||
|
||||
// Properties
|
||||
public var currentPage: Int?
|
||||
public var currentPageDidChange: (Int) -> Void = { _ in }
|
||||
let alignment: Alignment
|
||||
|
||||
var collectionWidth: CGFloat {
|
||||
guard let collection = collectionView else { return 0 }
|
||||
return collection.bounds.size.width - collection.contentInset.right - collection.contentInset.left
|
||||
}
|
||||
|
||||
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 = .horizontal
|
||||
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 HorizontalAlignmentLayout {
|
||||
|
||||
public override func prepare() {
|
||||
switch alignment {
|
||||
case .left:
|
||||
sectionInset.left = 0
|
||||
sectionInset.right = collectionWidth - itemSize.width
|
||||
case .center:
|
||||
let inset = (collectionWidth - itemSize.width) / 2
|
||||
sectionInset.left = inset
|
||||
sectionInset.right = inset
|
||||
case .right:
|
||||
sectionInset.right = 0
|
||||
sectionInset.left = collectionWidth - itemSize.width
|
||||
}
|
||||
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 proposedContentOffsetX: CGFloat
|
||||
switch alignment {
|
||||
case .left:
|
||||
proposedContentOffsetX = proposedContentOffset.x + itemSize.width / 2
|
||||
case .center:
|
||||
proposedContentOffsetX = proposedContentOffset.x + collectionView.bounds.width / 2
|
||||
case .right:
|
||||
proposedContentOffsetX = proposedContentOffset.x + collectionView.bounds.width
|
||||
}
|
||||
|
||||
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 .left:
|
||||
attributePosition = attributes.frame.minX
|
||||
candidatePosition = candidateAttributes!.frame.minX
|
||||
case .center:
|
||||
attributePosition = attributes.center.x
|
||||
candidatePosition = candidateAttributes!.center.x
|
||||
case .right:
|
||||
attributePosition = attributes.frame.maxX
|
||||
candidatePosition = candidateAttributes!.frame.maxX
|
||||
}
|
||||
|
||||
if abs(attributePosition - proposedContentOffsetX) < abs(candidatePosition - proposedContentOffsetX) {
|
||||
candidateAttributes = attributes
|
||||
}
|
||||
}
|
||||
|
||||
guard let aCandidateAttributes = candidateAttributes else {
|
||||
return proposedContentOffset
|
||||
}
|
||||
|
||||
var newOffsetX: CGFloat
|
||||
switch alignment {
|
||||
case .left:
|
||||
newOffsetX = aCandidateAttributes.frame.minX - collectionView.contentInset.left
|
||||
case .center:
|
||||
newOffsetX = aCandidateAttributes.center.x - collectionView.bounds.size.width / 2
|
||||
case .right:
|
||||
newOffsetX = aCandidateAttributes.frame.minX - collectionView.bounds.size.width + itemSize.width
|
||||
}
|
||||
|
||||
let offset = newOffsetX - collectionView.contentOffset.x
|
||||
|
||||
if (velocity.x < 0 && offset > 0) || (velocity.x > 0 && offset < 0) {
|
||||
let pageWidth = itemSize.width + minimumLineSpacing
|
||||
newOffsetX += velocity.x > 0 ? pageWidth : -pageWidth
|
||||
}
|
||||
|
||||
return CGPoint(x: newOffsetX, y: proposedContentOffset.y)
|
||||
}
|
||||
|
||||
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 .left:
|
||||
let middle = collectionView.contentInset.left + itemSize.width / 2
|
||||
center = collectionView.contentOffset.x + middle
|
||||
case .center:
|
||||
let middle = collectionView.frame.width / 2
|
||||
center = collectionView.contentOffset.x + middle
|
||||
case .right:
|
||||
let middle = collectionView.frame.width - collectionView.contentInset.right - itemSize.width / 2
|
||||
center = collectionView.contentOffset.x + middle
|
||||
}
|
||||
|
||||
let closestAttribute = layoutAttributes?.sorted {
|
||||
abs($0.center.x - center) < abs($1.center.x - 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,43 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
public final class PinnedHeaderFlowLayout: UICollectionViewFlowLayout {
|
||||
|
||||
private let header = UICollectionViewLayoutAttributes(forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, with: IndexPath(row: 0, section: 0))
|
||||
private var headerSize: CGSize = .zero
|
||||
|
||||
public override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
public override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
|
||||
guard let collectionView = self.collectionView else { return nil }
|
||||
let attributes = super.layoutAttributesForElements(in: rect)
|
||||
|
||||
let headerAttributes = attributes?.filter { $0.isFirstHeader }.first
|
||||
if let headerAttributes = headerAttributes {
|
||||
headerSize = headerAttributes.size
|
||||
}
|
||||
|
||||
if collectionView.contentOffset.y > 0 {
|
||||
header.frame.origin.y = collectionView.contentOffset.y
|
||||
} else {
|
||||
header.frame.origin.y = 0
|
||||
}
|
||||
|
||||
var newAttribures = attributes?.filter { !$0.isFirstHeader }
|
||||
header.frame.size = headerSize
|
||||
header.zIndex = Int.max
|
||||
newAttribures?.append(header)
|
||||
|
||||
return newAttribures
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private extension UICollectionViewLayoutAttributes {
|
||||
var isFirstHeader: Bool {
|
||||
guard let elementKind = representedElementKind else { return false }
|
||||
return (elementKind == UICollectionView.elementKindSectionHeader && indexPath.section == 0)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,60 +2,69 @@ import Foundation
|
||||
import UIKit
|
||||
|
||||
public struct DisplaySize {
|
||||
public static let iphone4 = CGSize(width: 320, height: 480)
|
||||
public static let iphone5 = CGSize(width: 320, height: 568)
|
||||
public static let iphone6 = CGSize(width: 375, height: 667)
|
||||
public static let iphone6plus = CGSize(width: 414, height: 736)
|
||||
public static let iphoneX = CGSize(width: 375, height: 812)
|
||||
public static let iPad9 = CGSize(width: 768, height: 1024)
|
||||
public static let ipad10 = CGSize(width: 834, height: 1112)
|
||||
public static let ipad12 = CGSize(width: 1024, height: 1366)
|
||||
public static let unknown = CGSize.zero
|
||||
public static let iphone4 = CGSize(width: 320, height: 480)
|
||||
public static let iphone5 = CGSize(width: 320, height: 568)
|
||||
public static let iphone6 = CGSize(width: 375, height: 667)
|
||||
public static let iphone6plus = CGSize(width: 414, height: 736)
|
||||
public static let iphoneX = CGSize(width: 375, height: 812)
|
||||
public static let iphoneXR = CGSize(width: 414, height: 896)
|
||||
public static let iPad9 = CGSize(width: 768, height: 1024)
|
||||
public static let ipad10 = CGSize(width: 834, height: 1112)
|
||||
public static let ipad12 = CGSize(width: 1024, height: 1366)
|
||||
public static let unknown = CGSize.zero
|
||||
}
|
||||
|
||||
public enum DisplayType {
|
||||
case unknown
|
||||
case iphone4
|
||||
case iphone5
|
||||
case iphone6
|
||||
case iphone6plus
|
||||
case iphoneX
|
||||
case ipad9
|
||||
case ipad10
|
||||
case ipad12
|
||||
case unknown
|
||||
case iphone4
|
||||
case iphone5
|
||||
case iphone6
|
||||
case iphone6plus
|
||||
case iphoneX
|
||||
case iphoneXR
|
||||
case iphoneXM
|
||||
case ipad9
|
||||
case ipad10
|
||||
case ipad12
|
||||
}
|
||||
|
||||
public enum Display {
|
||||
public static var width : CGFloat { return UIScreen.main.bounds.size.width }
|
||||
public static var height : CGFloat { return UIScreen.main.bounds.size.height }
|
||||
public static var maxLength : CGFloat { return max(width, height) }
|
||||
public static var minLength : CGFloat { return min(width, height) }
|
||||
public static var navbarSize : CGFloat { return Display.height == 812 ? 88 : 64 }
|
||||
public static var bottombarSize : CGFloat { return Display.height == 812 ? 34 : 0 }
|
||||
public static var zoomed : Bool { return UIScreen.main.nativeScale >= UIScreen.main.scale }
|
||||
public static var retina : Bool { return UIScreen.main.scale >= 2.0 }
|
||||
public static var phone : Bool { return UIDevice.current.userInterfaceIdiom == .phone }
|
||||
public static var pad : Bool { return UIDevice.current.userInterfaceIdiom == .pad }
|
||||
|
||||
public static var displayType: DisplayType {
|
||||
if phone && maxLength < 568 {
|
||||
return .iphone4
|
||||
} else if phone && maxLength == 568 {
|
||||
return .iphone5
|
||||
} else if phone && maxLength == 667 {
|
||||
return .iphone6
|
||||
} else if phone && maxLength == 736 {
|
||||
return .iphone6plus
|
||||
} else if phone && maxLength == 812 {
|
||||
return .iphoneX
|
||||
} else if pad && maxLength == 1024 {
|
||||
return .ipad9
|
||||
} else if pad && maxLength == 1112 {
|
||||
return .ipad10
|
||||
} else if pad && maxLength == 1366 {
|
||||
return .ipad12
|
||||
public static var width : CGFloat { return UIScreen.main.bounds.size.width }
|
||||
public static var height : CGFloat { return UIScreen.main.bounds.size.height }
|
||||
public static var maxLength : CGFloat { return max(width, height) }
|
||||
public static var minLength : CGFloat { return min(width, height) }
|
||||
public static var zoomed : Bool { return UIScreen.main.nativeScale >= UIScreen.main.scale }
|
||||
public static var retina : Bool { return UIScreen.main.scale >= 2.0 }
|
||||
public static var phone : Bool { return UIDevice.current.userInterfaceIdiom == .phone }
|
||||
public static var pad : Bool { return UIDevice.current.userInterfaceIdiom == .pad }
|
||||
|
||||
public static var navbarSize : CGFloat {
|
||||
return (Display.height == 812 || Display.height == 896) ? 88 : 64
|
||||
}
|
||||
public static var bottombarSize : CGFloat {
|
||||
return (Display.height == 812 || Display.height == 896) ? 34 : 0
|
||||
}
|
||||
|
||||
return .unknown
|
||||
}
|
||||
public static var displayType: DisplayType {
|
||||
if phone && maxLength < 568 {
|
||||
return .iphone4
|
||||
} else if phone && maxLength == 568 {
|
||||
return .iphone5
|
||||
} else if phone && maxLength == 667 {
|
||||
return .iphone6
|
||||
} else if phone && maxLength == 736 {
|
||||
return .iphone6plus
|
||||
} else if phone && maxLength == 896 {
|
||||
return .iphoneXR
|
||||
} else if phone && maxLength == 812 {
|
||||
return .iphoneX
|
||||
} else if pad && maxLength == 1024 {
|
||||
return .ipad9
|
||||
} else if pad && maxLength == 1112 {
|
||||
return .ipad10
|
||||
} else if pad && maxLength == 1366 {
|
||||
return .ipad12
|
||||
}
|
||||
return .unknown
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
@@ -201,10 +201,10 @@ extension CATransition {
|
||||
case fade, moveIn, push, reveal
|
||||
var value: String {
|
||||
switch self {
|
||||
case .fade: return kCATransitionFade
|
||||
case .moveIn: return kCATransitionMoveIn
|
||||
case .push: return kCATransitionPush
|
||||
case .reveal: return kCATransitionReveal
|
||||
case .fade: return convertFromCATransitionType(CATransitionType.fade)
|
||||
case .moveIn: return convertFromCATransitionType(CATransitionType.moveIn)
|
||||
case .push: return convertFromCATransitionType(CATransitionType.push)
|
||||
case .reveal: return convertFromCATransitionType(CATransitionType.reveal)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -213,19 +213,19 @@ extension CATransition {
|
||||
case fromRight, fromLeft, fromTop, fromBottom
|
||||
var value: String {
|
||||
switch self {
|
||||
case .fromRight: return kCATransitionFromRight
|
||||
case .fromLeft: return kCATransitionFromLeft
|
||||
case .fromTop: return kCATransitionFromTop
|
||||
case .fromBottom: return kCATransitionFromBottom
|
||||
case .fromRight: return convertFromCATransitionSubtype(CATransitionSubtype.fromRight)
|
||||
case .fromLeft: return convertFromCATransitionSubtype(CATransitionSubtype.fromLeft)
|
||||
case .fromTop: return convertFromCATransitionSubtype(CATransitionSubtype.fromTop)
|
||||
case .fromBottom: return convertFromCATransitionSubtype(CATransitionSubtype.fromBottom)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public convenience init(type: Kind, subtype: Subkind? = nil) {
|
||||
self.init()
|
||||
self.type = type.value
|
||||
self.type = convertToCATransitionType(type.value)
|
||||
if let subtype = subtype {
|
||||
self.subtype = subtype.value
|
||||
self.subtype = convertToOptionalCATransitionSubtype(subtype.value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,3 +247,24 @@ extension CATransaction {
|
||||
commit()
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function inserted by Swift 4.2 migrator.
|
||||
fileprivate func convertFromCATransitionType(_ input: CATransitionType) -> String {
|
||||
return input.rawValue
|
||||
}
|
||||
|
||||
// Helper function inserted by Swift 4.2 migrator.
|
||||
fileprivate func convertFromCATransitionSubtype(_ input: CATransitionSubtype) -> String {
|
||||
return input.rawValue
|
||||
}
|
||||
|
||||
// Helper function inserted by Swift 4.2 migrator.
|
||||
fileprivate func convertToCATransitionType(_ input: String) -> CATransitionType {
|
||||
return CATransitionType(rawValue: input)
|
||||
}
|
||||
|
||||
// Helper function inserted by Swift 4.2 migrator.
|
||||
fileprivate func convertToOptionalCATransitionSubtype(_ input: String?) -> CATransitionSubtype? {
|
||||
guard let input = input else { return nil }
|
||||
return CATransitionSubtype(rawValue: input)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -13,7 +13,7 @@ extension UIAlertController {
|
||||
- returns: self
|
||||
*/
|
||||
@discardableResult
|
||||
public func addAction(title: String?, style: UIAlertActionStyle = .default, handler: ((UIAlertAction) -> Void)? = nil) -> Self {
|
||||
public func addAction(title: String?, style: UIAlertAction.Style = .default, handler: ((UIAlertAction) -> Void)? = nil) -> Self {
|
||||
addAction(UIAlertAction(title: title, style: style, handler: handler))
|
||||
return self
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ extension UIButton {
|
||||
- parameter color: The background color to use for the specified state.
|
||||
- parameter state: The state that uses the specified image.
|
||||
*/
|
||||
public func setBackgroundColor(_ color: UIColor, for state: UIControlState) {
|
||||
public func setBackgroundColor(_ color: UIColor, for state: UIControl.State) {
|
||||
setBackgroundImage(UIImage(color: color), for: state)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: UIControlState){
|
||||
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: UIControlState){
|
||||
func set(image: UIImage?, attributedTitle title: NSAttributedString, titlePosition: Position, spacing: CGFloat, state: UIControl.State){
|
||||
imageView?.contentMode = .center
|
||||
setImage(image, for: state)
|
||||
|
||||
@@ -57,7 +57,7 @@ public extension UIButton {
|
||||
|
||||
// Use predefined font, otherwise use the default
|
||||
let titleFont: UIFont = titleLabel?.font ?? UIFont()
|
||||
let titleSize: CGSize = title.size(withAttributes: [NSAttributedStringKey.font: titleFont])
|
||||
let titleSize: CGSize = title.size(withAttributes: [NSAttributedString.Key.font: titleFont])
|
||||
|
||||
arrange(titleSize: titleSize, imageRect: imageRect, atPosition: position, spacing: spacing)
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ public extension UICollectionView {
|
||||
_ type: T.Type,
|
||||
withIdentifier reuseIdentifier: String = T.reuseIdentifier)
|
||||
{
|
||||
registerSupplement(T.self, kind: UICollectionElementKindSectionHeader)
|
||||
registerSupplement(T.self, kind: UICollectionView.elementKindSectionHeader)
|
||||
}
|
||||
|
||||
func dequeueHeader<T: UICollectionReusableView>(
|
||||
@@ -52,14 +52,14 @@ public extension UICollectionView {
|
||||
withIdentifier reuseIdentifier: String = T.reuseIdentifier,
|
||||
for indexPath: IndexPath) -> T
|
||||
{
|
||||
return dequeueSupplement(kind: UICollectionElementKindSectionHeader, for: indexPath)
|
||||
return dequeueSupplement(kind: UICollectionView.elementKindSectionHeader, for: indexPath)
|
||||
}
|
||||
|
||||
func registerFooter<T: UICollectionReusableView>(
|
||||
_ type: T.Type,
|
||||
withIdentifier reuseIdentifier: String = T.reuseIdentifier)
|
||||
{
|
||||
registerSupplement(T.self, kind: UICollectionElementKindSectionFooter)
|
||||
registerSupplement(T.self, kind: UICollectionView.elementKindSectionFooter)
|
||||
}
|
||||
|
||||
func dequeueFooter<T: UICollectionReusableView>(
|
||||
@@ -67,7 +67,7 @@ public extension UICollectionView {
|
||||
withIdentifier reuseIdentifier: String = T.reuseIdentifier,
|
||||
for indexPath: IndexPath) -> T
|
||||
{
|
||||
return dequeueSupplement(kind: UICollectionElementKindSectionFooter, for: indexPath)
|
||||
return dequeueSupplement(kind: UICollectionView.elementKindSectionFooter, for: indexPath)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ extension UIView {
|
||||
case tap
|
||||
case longPress
|
||||
case pan
|
||||
case swipe(UISwipeGestureRecognizerDirection)
|
||||
case swipe(UISwipeGestureRecognizer.Direction)
|
||||
case tapCount(Int)
|
||||
}
|
||||
|
||||
|
||||
@@ -106,7 +106,7 @@ extension UIImage {
|
||||
- parameter orientiation: The orientation to use for the scaled image (optional, defaults to the image's `imageOrientation` property).
|
||||
- returns: A copy of self, scaled by the scaleFactor (with an optional image orientation).
|
||||
*/
|
||||
public func scaled(by scaleFactor: CGFloat, withOrientation orientation: UIImageOrientation? = nil) -> UIImage? {
|
||||
public func scaled(by scaleFactor: CGFloat, withOrientation orientation: UIImage.Orientation? = nil) -> UIImage? {
|
||||
guard let coreImage = cgImage else { return nil }
|
||||
|
||||
return UIImage(cgImage: coreImage, scale: 1/scaleFactor, orientation: orientation ?? imageOrientation)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -4,11 +4,8 @@ extension UIScrollView {
|
||||
|
||||
func scrollToBottom() {
|
||||
layoutIfNeeded()
|
||||
|
||||
let minimumYOffset = -max(Display.navbarSize, contentInset.top)
|
||||
|
||||
contentOffset = CGPoint(x: 0,
|
||||
y: max(minimumYOffset, bounds.minY + contentSize.height + contentInset.top - bounds.height))
|
||||
let y = max(minimumYOffset, bounds.minY + contentSize.height + contentInset.top - bounds.height)
|
||||
contentOffset = CGPoint(x: 0, y: y)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -56,9 +56,9 @@ extension UIViewController {
|
||||
|
||||
extension UIViewController {
|
||||
public func add(_ child: UIViewController) {
|
||||
addChildViewController(child)
|
||||
addChild(child)
|
||||
view.addSubview(child.view)
|
||||
child.didMove(toParentViewController: self)
|
||||
child.didMove(toParent: self)
|
||||
}
|
||||
|
||||
public func remove() {
|
||||
@@ -66,8 +66,8 @@ extension UIViewController {
|
||||
return
|
||||
}
|
||||
|
||||
willMove(toParentViewController: nil)
|
||||
removeFromParentViewController()
|
||||
willMove(toParent: nil)
|
||||
removeFromParent()
|
||||
view.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ extension UIWindow {
|
||||
//mpdal controller have strong reference to it presenting controller, to correct memory management we must dismiss it before replace parent controller.
|
||||
previous?.dismiss(animated: false, completion: nil)
|
||||
previous?.view.removeFromSuperview()
|
||||
previous?.removeFromParentViewController()
|
||||
previous?.removeFromParent()
|
||||
}
|
||||
|
||||
guard let snapShot: UIView = subviews.last?.snapshotView() else {
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
class GestureRecognizerDelegate: NSObject, UIGestureRecognizerDelegate {
|
||||
var recognizerShouldBegin: ((UIGestureRecognizer) -> Bool)? = nil
|
||||
var shouldRecognizeSimultaneously: ((UIGestureRecognizer, UIGestureRecognizer) -> Bool)? = nil
|
||||
var shouldRequireFailureOf: ((UIGestureRecognizer, UIGestureRecognizer) -> Bool)? = nil
|
||||
var shouldRequireFailureBy: ((UIGestureRecognizer, UIGestureRecognizer) -> Bool)? = nil
|
||||
var shouldReceiveTouch: ((UIGestureRecognizer, UITouch) -> Bool)? = nil
|
||||
var shouldReceivePress: ((UIGestureRecognizer, UIPress) -> Bool)? = nil
|
||||
|
||||
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
return recognizerShouldBegin?(gestureRecognizer) ?? true
|
||||
}
|
||||
|
||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
return shouldRecognizeSimultaneously?(gestureRecognizer, otherGestureRecognizer) ?? true
|
||||
}
|
||||
|
||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
return shouldRequireFailureOf?(gestureRecognizer, otherGestureRecognizer) ?? true
|
||||
}
|
||||
|
||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
return shouldRequireFailureBy?(gestureRecognizer, otherGestureRecognizer) ?? false
|
||||
}
|
||||
|
||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
|
||||
return shouldReceiveTouch?(gestureRecognizer, touch) ?? true
|
||||
}
|
||||
|
||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive press: UIPress) -> Bool {
|
||||
return shouldReceivePress?(gestureRecognizer, press) ?? true
|
||||
}
|
||||
}
|
||||
+13
-13
@@ -32,22 +32,22 @@ public class InputVisibilityController: NSObject {
|
||||
|
||||
NotificationCenter.default.addObserver(self,
|
||||
selector: #selector(keyboardWillAppear),
|
||||
name: .UIKeyboardWillShow,
|
||||
name: UIResponder.keyboardWillShowNotification,
|
||||
object: nil)
|
||||
|
||||
NotificationCenter.default.addObserver(self,
|
||||
selector: #selector(keyboardWillDisappear),
|
||||
name: .UIKeyboardWillHide,
|
||||
name: UIResponder.keyboardWillHideNotification,
|
||||
object: nil)
|
||||
|
||||
NotificationCenter.default.addObserver(self,
|
||||
selector: #selector(applicationWillResign),
|
||||
name: .UIApplicationWillResignActive,
|
||||
name: UIApplication.willResignActiveNotification,
|
||||
object: nil)
|
||||
|
||||
NotificationCenter.default.addObserver(self,
|
||||
selector: #selector(keyboardWillChangeFrame),
|
||||
name: .UIKeyboardWillChangeFrame,
|
||||
name: UIResponder.keyboardWillChangeFrameNotification,
|
||||
object: nil)
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ public class InputVisibilityController: NSObject {
|
||||
}
|
||||
|
||||
@objc func keyboardWillChangeFrame(notification: Notification) {
|
||||
let keyboardEndFrame = (notification.userInfo![UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue
|
||||
let keyboardEndFrame = (notification.userInfo![UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue
|
||||
let goingUp = (keyboardEndFrame?.origin.y ?? 0) < UIScreen.main.bounds.height
|
||||
self.moveViewUp(up: goingUp, usingKeyboardNotification: notification)
|
||||
}
|
||||
@@ -92,7 +92,7 @@ public class InputVisibilityController: NSObject {
|
||||
}
|
||||
|
||||
let userInfo = notification.userInfo!
|
||||
let keyboardEndFrame = (userInfo[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue
|
||||
let keyboardEndFrame = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue
|
||||
|
||||
var toBeVisibleView = self.toBeVisibleView
|
||||
|
||||
@@ -106,11 +106,11 @@ public class InputVisibilityController: NSObject {
|
||||
// the old way of animation will match the keyboard animation timing and curve
|
||||
if !self.disableKeyboardMoveUpAnimation {
|
||||
UIView.beginAnimations(nil, context: nil)
|
||||
if let duration = userInfo[UIKeyboardAnimationDurationUserInfoKey] as? Double {
|
||||
if let duration = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double {
|
||||
UIView.setAnimationDuration(duration)
|
||||
}
|
||||
if let animationValue = userInfo[UIKeyboardAnimationCurveUserInfoKey] as? Int {
|
||||
if let animationCurve = UIViewAnimationCurve(rawValue: animationValue) {
|
||||
if let animationValue = userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as? Int {
|
||||
if let animationCurve = UIView.AnimationCurve(rawValue: animationValue) {
|
||||
UIView.setAnimationCurve(animationCurve)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -18,28 +18,28 @@ public enum KeyboardEventType {
|
||||
|
||||
public var notificationName: NSNotification.Name {
|
||||
switch self {
|
||||
case .willShow: return .UIKeyboardWillShow
|
||||
case .didShow: return .UIKeyboardDidShow
|
||||
case .willHide: return .UIKeyboardWillHide
|
||||
case .didHide: return .UIKeyboardDidHide
|
||||
case .willChangeFrame: return .UIKeyboardWillChangeFrame
|
||||
case .didChangeFrame: return .UIKeyboardDidChangeFrame
|
||||
case .willShow: return UIResponder.keyboardWillShowNotification
|
||||
case .didShow: return UIResponder.keyboardDidShowNotification
|
||||
case .willHide: return UIResponder.keyboardWillHideNotification
|
||||
case .didHide: return UIResponder.keyboardDidHideNotification
|
||||
case .willChangeFrame: return UIResponder.keyboardWillChangeFrameNotification
|
||||
case .didChangeFrame: return UIResponder.keyboardDidChangeFrameNotification
|
||||
}
|
||||
}
|
||||
|
||||
init?(name: NSNotification.Name) {
|
||||
switch name {
|
||||
case NSNotification.Name.UIKeyboardWillShow:
|
||||
case UIResponder.keyboardWillShowNotification:
|
||||
self = .willShow
|
||||
case NSNotification.Name.UIKeyboardDidShow:
|
||||
case UIResponder.keyboardDidShowNotification:
|
||||
self = .didShow
|
||||
case NSNotification.Name.UIKeyboardWillHide:
|
||||
case UIResponder.keyboardWillHideNotification:
|
||||
self = .willHide
|
||||
case NSNotification.Name.UIKeyboardDidHide:
|
||||
case UIResponder.keyboardDidHideNotification:
|
||||
self = .didHide
|
||||
case NSNotification.Name.UIKeyboardWillChangeFrame:
|
||||
case UIResponder.keyboardWillChangeFrameNotification:
|
||||
self = .willChangeFrame
|
||||
case NSNotification.Name.UIKeyboardDidChangeFrame:
|
||||
case UIResponder.keyboardDidChangeFrameNotification:
|
||||
self = .didChangeFrame
|
||||
default:
|
||||
return nil
|
||||
@@ -62,27 +62,27 @@ public struct KeyboardEvent {
|
||||
public let type: KeyboardEventType
|
||||
public let keyboardFrameBegin: CGRect
|
||||
public let keyboardFrameEnd: CGRect
|
||||
public let curve: UIViewAnimationCurve
|
||||
public let curve: UIView.AnimationCurve
|
||||
public let duration: TimeInterval
|
||||
public let isLocal: Bool
|
||||
|
||||
public var options: UIViewAnimationOptions {
|
||||
return UIViewAnimationOptions(rawValue: UInt(curve.rawValue << 16))
|
||||
public var options: UIView.AnimationOptions {
|
||||
return UIView.AnimationOptions(rawValue: UInt(curve.rawValue << 16))
|
||||
}
|
||||
|
||||
init?(notification: Notification) {
|
||||
guard let userInfo = (notification as NSNotification).userInfo else { return nil }
|
||||
guard let type = KeyboardEventType(name: notification.name) else { return nil }
|
||||
guard let begin = (userInfo[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue else { return nil }
|
||||
guard let end = (userInfo[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue else { return nil }
|
||||
guard let begin = (userInfo[UIResponder.keyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue else { return nil }
|
||||
guard let end = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue else { return nil }
|
||||
guard
|
||||
let curveInt = (userInfo[UIKeyboardAnimationCurveUserInfoKey] as? NSNumber)?.intValue,
|
||||
let curve = UIViewAnimationCurve(rawValue: curveInt)
|
||||
let curveInt = (userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as? NSNumber)?.intValue,
|
||||
let curve = UIView.AnimationCurve(rawValue: curveInt)
|
||||
else { return nil }
|
||||
guard
|
||||
let durationDouble = (userInfo[UIKeyboardAnimationDurationUserInfoKey] as? NSNumber)?.doubleValue
|
||||
let durationDouble = (userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber)?.doubleValue
|
||||
else { return nil }
|
||||
guard let isLocalInt = (userInfo[UIKeyboardIsLocalUserInfoKey] as? NSNumber)?.intValue
|
||||
guard let isLocalInt = (userInfo[UIResponder.keyboardIsLocalUserInfoKey] as? NSNumber)?.intValue
|
||||
else { return nil }
|
||||
|
||||
self.type = type
|
||||
|
||||
@@ -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,27 +2,37 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
#if !os(tvOS)
|
||||
extension UISwitch {
|
||||
public override func selectedToggle(select: Bool) {
|
||||
setOn(select, animated: true)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
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) {
|
||||
@@ -30,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) {
|
||||
@@ -62,10 +74,10 @@ public class ButtonsTogglerView: UIStackView {
|
||||
|
||||
public required init(defaultIndex index: Int = 0,
|
||||
buttons: [UIButton],
|
||||
axis: UILayoutConstraintAxis = .horizontal,
|
||||
axis: NSLayoutConstraint.Axis = .horizontal,
|
||||
spacing: CGFloat = 30,
|
||||
distribution: UIStackViewDistribution = .equalSpacing,
|
||||
alignment: UIStackViewAlignment = .fill) {
|
||||
distribution: UIStackView.Distribution = .equalSpacing,
|
||||
alignment: UIStackView.Alignment = .fill) {
|
||||
|
||||
currentIndex = index
|
||||
toggler = Toggler(default: index, togglers: buttons)
|
||||
|
||||
@@ -3,134 +3,139 @@ import UIKit
|
||||
extension UIViewController {
|
||||
|
||||
public func findUp<T>() -> T? where T: UIViewController {
|
||||
return findUpVC(self)
|
||||
return UISearchUtilities.findUpVC(self)
|
||||
}
|
||||
|
||||
public func findDown<T>() -> T? where T: UIViewController {
|
||||
return findDownVC(self)
|
||||
return UISearchUtilities.findDownVC(self)
|
||||
}
|
||||
}
|
||||
|
||||
func findUpVC<T>(_ base: UIViewController) -> T? where T: UIViewController {
|
||||
|
||||
var vc: UIViewController? = base
|
||||
|
||||
while vc != nil {
|
||||
if let vc = vc?.parent as? T {
|
||||
return vc
|
||||
} else {
|
||||
vc = vc?.parent
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func findDownVC<T>(_ base: UIViewController) -> T? where T: UIViewController {
|
||||
var result: T?
|
||||
for c in base.childViewControllers {
|
||||
if let r = c as? T {
|
||||
result = r
|
||||
} else {
|
||||
result = findDownVC(c)
|
||||
struct UISearchUtilities {
|
||||
|
||||
static func findUpVC<T>(_ base: UIViewController) -> T? where T: UIViewController {
|
||||
|
||||
var vc: UIViewController? = base
|
||||
|
||||
while vc != nil {
|
||||
if let vc = vc?.parent as? T {
|
||||
return vc
|
||||
} else {
|
||||
vc = vc?.parent
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if result != nil {
|
||||
break
|
||||
|
||||
static func findDownVC<T>(_ base: UIViewController) -> T? where T: UIViewController {
|
||||
var result: T?
|
||||
for c in base.children {
|
||||
if let r = c as? T {
|
||||
result = r
|
||||
} else {
|
||||
result = findDownVC(c)
|
||||
}
|
||||
if result != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
static func findDownVC<T>() -> T? where T: UIViewController {
|
||||
guard let base = UIApplication.shared.keyWindow?.rootViewController else { return nil }
|
||||
if let result = base as? T {
|
||||
return result
|
||||
}
|
||||
return findDownVC(base)
|
||||
}
|
||||
|
||||
static func first(inView view: UIView, where condition: @escaping (UIView) -> Bool) -> UIView? {
|
||||
guard !condition(view) else { return view }
|
||||
|
||||
var result: UIView?
|
||||
for v in view.subviews {
|
||||
if condition(v) {
|
||||
result = v
|
||||
} else {
|
||||
result = first(inView: v, where: condition)
|
||||
}
|
||||
if result != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
static func findView<T>(_ root: UIView) -> T? where T: UIView {
|
||||
var result: T?
|
||||
for v in root.subviews {
|
||||
if let r = v as? T {
|
||||
result = r
|
||||
} else {
|
||||
result = findView(v)
|
||||
}
|
||||
if result != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
static func findSuperView<T>(_ base: UIView) -> T? where T: UIView {
|
||||
var v: UIView? = base
|
||||
while v != nil {
|
||||
if let v = v?.superview as? T {
|
||||
return v
|
||||
} else {
|
||||
v = v?.superview
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
static func findAllViews<T>(_ root: UIView) -> [T] where T: UIView {
|
||||
var result: [T] = []
|
||||
for v in root.subviews {
|
||||
if let r = v as? T {
|
||||
result.append(r)
|
||||
}
|
||||
let rv: [T] = findAllViews(v)
|
||||
result.append(contentsOf: rv)
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func findDownVC<T>() -> T? where T: UIViewController {
|
||||
guard let base = UIApplication.shared.keyWindow?.rootViewController else { return nil }
|
||||
if let result = base as? T {
|
||||
return result
|
||||
}
|
||||
return findDownVC(base)
|
||||
}
|
||||
|
||||
extension UIView {
|
||||
|
||||
public func first(where condition: @escaping (UIView) -> Bool) -> UIView? {
|
||||
return Utopia.first(inView: self, where: condition)
|
||||
return UISearchUtilities.first(inView: self, where: condition)
|
||||
}
|
||||
|
||||
public func find<T>() -> T? where T: UIView {
|
||||
return Utopia.findView(self)
|
||||
return UISearchUtilities.findView(self)
|
||||
}
|
||||
|
||||
public func findSuperView<T>() -> T? where T: UIView {
|
||||
return Utopia.findSuperView(self)
|
||||
return UISearchUtilities.findSuperView(self)
|
||||
}
|
||||
|
||||
public func findAll<T>() -> [T] where T: UIView {
|
||||
return findAllViews(self)
|
||||
return UISearchUtilities.findAllViews(self)
|
||||
}
|
||||
}
|
||||
|
||||
func first(inView view: UIView, where condition: @escaping (UIView) -> Bool) -> UIView? {
|
||||
guard !condition(view) else { return view }
|
||||
|
||||
var result: UIView?
|
||||
for v in view.subviews {
|
||||
if condition(v) {
|
||||
result = v
|
||||
} else {
|
||||
result = first(inView: v, where: condition)
|
||||
}
|
||||
if result != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func findView<T>(_ root: UIView) -> T? where T: UIView {
|
||||
var result: T?
|
||||
for v in root.subviews {
|
||||
if let r = v as? T {
|
||||
result = r
|
||||
} else {
|
||||
result = findView(v)
|
||||
}
|
||||
if result != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func findSuperView<T>(_ base: UIView) -> T? where T: UIView {
|
||||
var v: UIView? = base
|
||||
while v != nil {
|
||||
if let v = v?.superview as? T {
|
||||
return v
|
||||
} else {
|
||||
v = v?.superview
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func findAllViews<T>(_ root: UIView) -> [T] where T: UIView {
|
||||
var result: [T] = []
|
||||
for v in root.subviews {
|
||||
if let r = v as? T {
|
||||
result.append(r)
|
||||
}
|
||||
let rv: [T] = findAllViews(v)
|
||||
result.append(contentsOf: rv)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
extension UIView {
|
||||
|
||||
public func findConstraints(attribute: NSLayoutAttribute) -> [NSLayoutConstraint] {
|
||||
public func findConstraints(attribute: NSLayoutConstraint.Attribute) -> [NSLayoutConstraint] {
|
||||
let result = constraints.filter { $0.firstAttribute == attribute && $0.firstItem as? NSObject == self }
|
||||
return result
|
||||
}
|
||||
|
||||
public func findSuperviewConstraints(attribute: NSLayoutAttribute) -> [NSLayoutConstraint] {
|
||||
public func findSuperviewConstraints(attribute: NSLayoutConstraint.Attribute) -> [NSLayoutConstraint] {
|
||||
let result = superview?.constraints.filter {
|
||||
($0.firstAttribute == attribute && $0.firstItem as? NSObject == self) ||
|
||||
($0.secondAttribute == attribute && $0.secondItem as? NSObject == self)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -7,9 +7,9 @@ private class Associated<T>: NSObject {
|
||||
}
|
||||
}
|
||||
|
||||
protocol Associable {}
|
||||
public protocol Associable {}
|
||||
|
||||
extension Associable where Self: AnyObject {
|
||||
public extension Associable where Self: AnyObject {
|
||||
|
||||
func getAssociatedObject<T>(_ key: UnsafeRawPointer) -> T? {
|
||||
return (objc_getAssociatedObject(self, key) as? Associated<T>).map { $0.value }
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
//
|
||||
// DiffAware.swift
|
||||
// DeepDiff
|
||||
//
|
||||
// Created by Khoa Pham on 03.01.2018.
|
||||
// Copyright © 2018 Khoa Pham. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public protocol DiffAware {
|
||||
func diff<T: Hashable>(old: Array<T>, new: Array<T>) -> [Change<T>]
|
||||
}
|
||||
|
||||
extension DiffAware {
|
||||
func preprocess<T: Hashable>(old: Array<T>, new: Array<T>) -> [Change<T>]? {
|
||||
switch (old.isEmpty, new.isEmpty) {
|
||||
case (true, true):
|
||||
// empty
|
||||
return []
|
||||
case (true, false):
|
||||
// all .insert
|
||||
return new.enumerated().map { index, item in
|
||||
return .insert(Insert(item: item, index: index))
|
||||
}
|
||||
case (false, true):
|
||||
// all .delete
|
||||
return old.enumerated().map { index, item in
|
||||
return .delete(Delete(item: item, index: index))
|
||||
}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,278 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
// https://gist.github.com/ndarville/3166060
|
||||
|
||||
public final class Heckel: DiffAware {
|
||||
|
||||
// OC and NC can assume three values: 1, 2, and many.
|
||||
enum Counter {
|
||||
case zero, one, many
|
||||
|
||||
func increment() -> Counter {
|
||||
switch self {
|
||||
case .zero:
|
||||
return .one
|
||||
case .one:
|
||||
return .many
|
||||
case .many:
|
||||
return self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The symbol table stores three entries for each line
|
||||
class TableEntry: Equatable {
|
||||
// The value entry for each line in table has two counters.
|
||||
// They specify the line's number of occurrences in O and N: OC and NC.
|
||||
var oldCounter: Counter = .zero
|
||||
var newCounter: Counter = .zero
|
||||
|
||||
// Aside from the two counters, the line's entry
|
||||
// also includes a reference to the line's line number in O: OLNO.
|
||||
// OLNO is only interesting, if OC == 1.
|
||||
// Alternatively, OLNO would have to assume multiple values or none at all.
|
||||
var indexesInOld: [Int] = []
|
||||
|
||||
static func ==(lhs: TableEntry, rhs: TableEntry) -> Bool {
|
||||
return lhs.oldCounter == rhs.oldCounter && lhs.newCounter == rhs.newCounter && lhs.indexesInOld == rhs.indexesInOld
|
||||
}
|
||||
}
|
||||
|
||||
// The arrays OA and NA have one entry for each line in their respective files, O and N.
|
||||
// The arrays contain either:
|
||||
enum ArrayEntry: Equatable {
|
||||
// a pointer to the line's symbol table entry, table[line]
|
||||
case tableEntry(TableEntry)
|
||||
|
||||
// the line's number in the other file (N for OA, O for NA)
|
||||
case indexInOther(Int)
|
||||
|
||||
public static func == (lhs: ArrayEntry, rhs: ArrayEntry) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case (.tableEntry(let l), .tableEntry(let r)):
|
||||
return l == r
|
||||
case (.indexInOther(let l), .indexInOther(let r)):
|
||||
return l == r
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public init() {}
|
||||
|
||||
public func diff<T: Hashable>(old: Array<T>, new: Array<T>) -> [Change<T>] {
|
||||
// The Symbol Table
|
||||
// Each line works as the key in the table look-up, i.e. as table[line].
|
||||
var table: [Int: TableEntry] = [:]
|
||||
|
||||
// The arrays OA and NA have one entry for each line in their respective files, O and N
|
||||
var oldArray = [ArrayEntry]()
|
||||
var newArray = [ArrayEntry]()
|
||||
|
||||
perform1stPass(new: new, table: &table, newArray: &newArray)
|
||||
perform2ndPass(old: old, table: &table, oldArray: &oldArray)
|
||||
perform345Pass(newArray: &newArray, oldArray: &oldArray)
|
||||
let changes = perform6thPass(new: new, old: old, newArray: newArray, oldArray: oldArray)
|
||||
return changes
|
||||
}
|
||||
|
||||
private func perform1stPass<T: Hashable>(
|
||||
new: Array<T>,
|
||||
table: inout [Int: TableEntry],
|
||||
newArray: inout [ArrayEntry]) {
|
||||
|
||||
// 1st pass
|
||||
// a. Each line i of file N is read in sequence
|
||||
new.forEach { item in
|
||||
// b. An entry for each line i is created in the table, if it doesn't already exist
|
||||
let entry = table[item.hashValue] ?? TableEntry()
|
||||
|
||||
// c. NC for the line's table entry is incremented
|
||||
entry.newCounter = entry.newCounter.increment()
|
||||
|
||||
// d. NA[i] is set to point to the table entry of line i
|
||||
newArray.append(.tableEntry(entry))
|
||||
|
||||
//
|
||||
table[item.hashValue] = entry
|
||||
}
|
||||
}
|
||||
|
||||
private func perform2ndPass<T: Hashable>(
|
||||
old: Array<T>,
|
||||
table: inout [Int: TableEntry],
|
||||
oldArray: inout [ArrayEntry]) {
|
||||
|
||||
// 2nd pass
|
||||
// Similar to first pass, except it acts on files
|
||||
|
||||
old.enumerated().forEach { tuple in
|
||||
// old
|
||||
let entry = table[tuple.element.hashValue] ?? TableEntry()
|
||||
|
||||
// oldCounter
|
||||
entry.oldCounter = entry.oldCounter.increment()
|
||||
|
||||
// lineNumberInOld which is set to the line's number
|
||||
entry.indexesInOld.append(tuple.offset)
|
||||
|
||||
// oldArray
|
||||
oldArray.append(.tableEntry(entry))
|
||||
|
||||
//
|
||||
table[tuple.element.hashValue] = entry
|
||||
}
|
||||
}
|
||||
|
||||
private func perform345Pass(newArray: inout [ArrayEntry], oldArray: inout [ArrayEntry]) {
|
||||
// 3rd pass
|
||||
// a. We use Observation 1:
|
||||
// If a line occurs only once in each file, then it must be the same line,
|
||||
// although it may have been moved.
|
||||
// We use this observation to locate unaltered lines that we
|
||||
// subsequently exclude from further treatment.
|
||||
// b. Using this, we only process the lines where OC == NC == 1
|
||||
// c. As the lines between O and N "must be the same line,
|
||||
// although it may have been moved", we alter the table pointers
|
||||
// in OA and NA to the number of the line in the other file.
|
||||
// d. We also locate unique virtual lines
|
||||
// immediately before the first and
|
||||
// immediately after the last lines of the files ???
|
||||
//
|
||||
// 4th pass
|
||||
// a. We use Observation 2:
|
||||
// If a line has been found to be unaltered,
|
||||
// and the lines immediately adjacent to it in both files are identical,
|
||||
// then these lines must be the same line.
|
||||
// This information can be used to find blocks of unchanged lines.
|
||||
// b. Using this, we process each entry in ascending order.
|
||||
// c. If
|
||||
// NA[i] points to OA[j], and
|
||||
// NA[i+1] and OA[j+1] contain identical table entry pointers
|
||||
// then
|
||||
// OA[j+1] is set to line i+1, and
|
||||
// NA[i+1] is set to line j+1
|
||||
//
|
||||
// 5th pass
|
||||
// Similar to fourth pass, except:
|
||||
// It processes each entry in descending order
|
||||
// It uses j-1 and i-1 instead of j+1 and i+1
|
||||
|
||||
newArray.enumerated().forEach { (indexOfNew, item) in
|
||||
switch item {
|
||||
case .tableEntry(let entry):
|
||||
guard !entry.indexesInOld.isEmpty else {
|
||||
return
|
||||
}
|
||||
let indexOfOld = entry.indexesInOld.removeFirst()
|
||||
let isObservation1 = entry.newCounter == .one && entry.oldCounter == .one
|
||||
let isObservation2 = entry.newCounter != .zero && entry.oldCounter != .zero && newArray[indexOfNew] == oldArray[indexOfOld]
|
||||
guard isObservation1 || isObservation2 else {
|
||||
return
|
||||
}
|
||||
newArray[indexOfNew] = .indexInOther(indexOfOld)
|
||||
oldArray[indexOfOld] = .indexInOther(indexOfNew)
|
||||
case .indexInOther(_):
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func perform6thPass<T: Hashable>(
|
||||
new: Array<T>,
|
||||
old: Array<T>,
|
||||
newArray: [ArrayEntry],
|
||||
oldArray: [ArrayEntry]) -> [Change<T>] {
|
||||
|
||||
// 6th pass
|
||||
// At this point following our five passes,
|
||||
// we have the necessary information contained in NA to tell the differences between O and N.
|
||||
// This pass uses NA and OA to tell when a line has changed between O and N,
|
||||
// and how far the change extends.
|
||||
|
||||
// a. Determining a New Line
|
||||
// Recall our initial description of NA in which we said that the array has either:
|
||||
// one entry for each line of file N containing either
|
||||
// a pointer to table[line]
|
||||
// the line's number in file O
|
||||
|
||||
// Using these two cases, we know that if NA[i] refers
|
||||
// to an entry in table (case 1), then line i must be new
|
||||
// We know this, because otherwise, NA[i] would have contained
|
||||
// the line's number in O (case 2), if it existed in O and N
|
||||
|
||||
// b. Determining the Boundaries of the New Line
|
||||
// We now know that we are dealing with a new line, but we have yet to figure where the change ends.
|
||||
// Recall Observation 2:
|
||||
|
||||
// If NA[i] points to OA[j], but NA[i+1] does not
|
||||
// point to OA[j+1], then line i is the boundary for the alteration.
|
||||
|
||||
// You can look at it this way:
|
||||
// i : The quick brown fox | j : The quick brown fox
|
||||
// i+1: jumps over the lazy dog | j+1: jumps over the loafing cat
|
||||
|
||||
// Here, NA[i] == OA[j], but NA[i+1] != OA[j+1].
|
||||
// This means our boundary is between the two lines.
|
||||
|
||||
var changes = [Change<T>]()
|
||||
var deleteOffsets = Array(repeating: 0, count: old.count)
|
||||
|
||||
// deletions
|
||||
do {
|
||||
var runningOffset = 0
|
||||
|
||||
oldArray.enumerated().forEach { oldTuple in
|
||||
deleteOffsets[oldTuple.offset] = runningOffset
|
||||
|
||||
guard case .tableEntry = oldTuple.element else {
|
||||
return
|
||||
}
|
||||
|
||||
changes.append(.delete(Delete(
|
||||
item: old[oldTuple.offset],
|
||||
index: oldTuple.offset
|
||||
)))
|
||||
|
||||
runningOffset += 1
|
||||
}
|
||||
}
|
||||
|
||||
// insertions, replaces, moves
|
||||
do {
|
||||
var runningOffset = 0
|
||||
|
||||
newArray.enumerated().forEach { newTuple in
|
||||
switch newTuple.element {
|
||||
case .tableEntry:
|
||||
runningOffset += 1
|
||||
changes.append(.insert(Insert(
|
||||
item: new[newTuple.offset],
|
||||
index: newTuple.offset
|
||||
)))
|
||||
case .indexInOther(let oldIndex):
|
||||
if old[oldIndex] != new[newTuple.offset] {
|
||||
changes.append(.replace(Replace(
|
||||
oldItem: old[oldIndex],
|
||||
newItem: new[newTuple.offset],
|
||||
index: newTuple.offset
|
||||
)))
|
||||
}
|
||||
|
||||
let deleteOffset = deleteOffsets[oldIndex]
|
||||
// The object is not at the expected position, so move it.
|
||||
if (oldIndex - deleteOffset + runningOffset) != newTuple.offset {
|
||||
changes.append(.move(Move(
|
||||
item: new[newTuple.offset],
|
||||
fromIndex: oldIndex,
|
||||
toIndex: newTuple.offset
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return changes
|
||||
}
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
// https://en.wikipedia.org/wiki/Wagner%E2%80%93Fischer_algorithm
|
||||
|
||||
public final class WagnerFischer: DiffAware {
|
||||
private let reduceMove: Bool
|
||||
|
||||
public init(reduceMove: Bool = false) {
|
||||
self.reduceMove = reduceMove
|
||||
}
|
||||
|
||||
public func diff<T: Hashable>(old: Array<T>, new: Array<T>) -> [Change<T>] {
|
||||
let previousRow = Row<T>()
|
||||
previousRow.seed(with: new)
|
||||
let currentRow = Row<T>()
|
||||
|
||||
// row in matrix
|
||||
old.enumerated().forEach { indexInOld, oldItem in
|
||||
// reset current row
|
||||
currentRow.reset(
|
||||
count: previousRow.slots.count,
|
||||
indexInOld: indexInOld,
|
||||
oldItem: oldItem
|
||||
)
|
||||
|
||||
// column in matrix
|
||||
new.enumerated().forEach { indexInNew, newItem in
|
||||
if isEqual(oldItem: old[indexInOld], newItem: new[indexInNew]) {
|
||||
currentRow.update(indexInNew: indexInNew, previousRow: previousRow)
|
||||
} else {
|
||||
currentRow.updateWithMin(
|
||||
previousRow: previousRow,
|
||||
indexInNew: indexInNew,
|
||||
newItem: newItem,
|
||||
indexInOld: indexInOld,
|
||||
oldItem: oldItem
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// set previousRow
|
||||
previousRow.slots = currentRow.slots
|
||||
}
|
||||
|
||||
let changes = currentRow.lastSlot()
|
||||
if reduceMove {
|
||||
return MoveReducer<T>().reduce(changes: changes)
|
||||
} else {
|
||||
return changes
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helper
|
||||
|
||||
private func isEqual<T: Hashable>(oldItem: T, newItem: T) -> Bool {
|
||||
// Same items must have same hashValue
|
||||
if oldItem.hashValue != newItem.hashValue {
|
||||
return false
|
||||
} else {
|
||||
// Different hashValue does not always mean different items
|
||||
return oldItem == newItem
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We can adapt the algorithm to use less space, O(m) instead of O(mn),
|
||||
// since it only requires that the previous row and current row be stored at any one time
|
||||
class Row<T> {
|
||||
/// Each slot is a collection of Change
|
||||
var slots: [[Change<T>]] = []
|
||||
|
||||
/// Seed with .insert from new
|
||||
func seed(with new: Array<T>) {
|
||||
// First slot should be empty
|
||||
slots = Array(repeatElement([], count: new.count + 1))
|
||||
|
||||
// Each slot increases in the number of changes
|
||||
new.enumerated().forEach { index, item in
|
||||
let slotIndex = convert(indexInNew: index)
|
||||
slots[slotIndex] = combine(
|
||||
slot: slots[slotIndex-1],
|
||||
change: .insert(Insert(item: item, index: index))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Reset with empty slots
|
||||
/// First slot is .delete
|
||||
func reset(count: Int, indexInOld: Int, oldItem: T) {
|
||||
if slots.isEmpty {
|
||||
slots = Array(repeatElement([], count: count))
|
||||
}
|
||||
|
||||
slots[0] = combine(
|
||||
slot: slots[0],
|
||||
change: .delete(Delete(item: oldItem, index: indexInOld))
|
||||
)
|
||||
}
|
||||
|
||||
/// Use .replace from previousRow
|
||||
func update(indexInNew: Int, previousRow: Row) {
|
||||
let slotIndex = convert(indexInNew: indexInNew)
|
||||
slots[slotIndex] = previousRow.slots[slotIndex - 1]
|
||||
}
|
||||
|
||||
/// Choose the min
|
||||
func updateWithMin(previousRow: Row, indexInNew: Int, newItem: T, indexInOld: Int, oldItem: T) {
|
||||
let slotIndex = convert(indexInNew: indexInNew)
|
||||
let topSlot = previousRow.slots[slotIndex]
|
||||
let leftSlot = slots[slotIndex - 1]
|
||||
let topLeftSlot = previousRow.slots[slotIndex - 1]
|
||||
|
||||
let minCount = min(topSlot.count, leftSlot.count, topLeftSlot.count)
|
||||
|
||||
// Order of cases does not matter
|
||||
switch minCount {
|
||||
case topSlot.count:
|
||||
slots[slotIndex] = combine(
|
||||
slot: topSlot,
|
||||
change: .delete(Delete(item: oldItem, index: indexInOld))
|
||||
)
|
||||
case leftSlot.count:
|
||||
slots[slotIndex] = combine(
|
||||
slot: leftSlot,
|
||||
change: .insert(Insert(item: newItem, index: indexInNew))
|
||||
)
|
||||
case topLeftSlot.count:
|
||||
slots[slotIndex] = combine(
|
||||
slot: topLeftSlot,
|
||||
change: .replace(Replace(oldItem: oldItem, newItem: newItem, index: indexInNew))
|
||||
)
|
||||
default:
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
|
||||
/// Add one more change
|
||||
func combine<T>(slot: [Change<T>], change: Change<T>) -> [Change<T>] {
|
||||
var slot = slot
|
||||
slot.append(change)
|
||||
return slot
|
||||
}
|
||||
|
||||
//// Last slot
|
||||
func lastSlot() -> [Change<T>] {
|
||||
return slots[slots.count - 1]
|
||||
}
|
||||
|
||||
/// Convert to slotIndex, as slots has 1 extra at the beginning
|
||||
func convert(indexInNew: Int) -> Int {
|
||||
return indexInNew + 1
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
public struct Insert<T> {
|
||||
public let item: T
|
||||
public let index: Int
|
||||
}
|
||||
|
||||
public struct Delete<T> {
|
||||
public let item: T
|
||||
public let index: Int
|
||||
}
|
||||
|
||||
public struct Replace<T> {
|
||||
public let oldItem: T
|
||||
public let newItem: T
|
||||
public let index: Int
|
||||
}
|
||||
|
||||
public struct Move<T> {
|
||||
public let item: T
|
||||
public let fromIndex: Int
|
||||
public let toIndex: Int
|
||||
}
|
||||
|
||||
/// The computed changes from diff
|
||||
///
|
||||
/// - insert: Insert an item at index
|
||||
/// - delete: Delete an item from index
|
||||
/// - replace: Replace an item at index with another item
|
||||
/// - move: Move the same item from this index to another index
|
||||
public enum Change<T> {
|
||||
case insert(Insert<T>)
|
||||
case delete(Delete<T>)
|
||||
case replace(Replace<T>)
|
||||
case move(Move<T>)
|
||||
|
||||
public var insert: Insert<T>? {
|
||||
if case .insert(let insert) = self {
|
||||
return insert
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
public var delete: Delete<T>? {
|
||||
if case .delete(let delete) = self {
|
||||
return delete
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
public var replace: Replace<T>? {
|
||||
if case .replace(let replace) = self {
|
||||
return replace
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
public var move: Move<T>? {
|
||||
if case .move(let move) = self {
|
||||
return move
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
extension Collection {
|
||||
func executeIfPresent(_ closure: (Self) -> Void) {
|
||||
if !isEmpty {
|
||||
closure(self)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Array where Element == Int {
|
||||
var asIndexSet: IndexSet {
|
||||
return reduce(IndexSet()) { set, item in
|
||||
var set = set
|
||||
set.insert(item)
|
||||
return set
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
/// Perform diff between old and new collections
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - old: Old collection
|
||||
/// - new: New collection
|
||||
/// - Returns: A set of changes
|
||||
public func diff<T: Hashable>(
|
||||
old: Array<T>,
|
||||
new: Array<T>,
|
||||
algorithm: DiffAware = Heckel()) -> [Change<T>] {
|
||||
|
||||
if let changes = algorithm.preprocess(old: old, new: new) {
|
||||
return changes
|
||||
}
|
||||
|
||||
return algorithm.diff(old: old, new: new)
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
struct MoveReducer<T> {
|
||||
func reduce<T: Equatable>(changes: [Change<T>]) -> [Change<T>] {
|
||||
// Find pairs of .insert and .delete with same item
|
||||
let inserts = changes.compactMap({ $0.insert })
|
||||
|
||||
if inserts.isEmpty {
|
||||
return changes
|
||||
}
|
||||
|
||||
var changes = changes
|
||||
inserts.forEach { insert in
|
||||
if let insertIndex = changes.index(where: { $0.insert?.item == insert.item }),
|
||||
let deleteIndex = changes.index(where: { $0.delete?.item == insert.item }) {
|
||||
|
||||
let insertChange = changes[insertIndex].insert!
|
||||
let deleteChange = changes[deleteIndex].delete!
|
||||
|
||||
let move = Move<T>(item: insert.item, fromIndex: deleteChange.index, toIndex: insertChange.index)
|
||||
|
||||
// .insert can be before or after .delete
|
||||
let minIndex = min(insertIndex, deleteIndex)
|
||||
let maxIndex = max(insertIndex, deleteIndex)
|
||||
|
||||
// remove both .insert and .delete, and replace by .move
|
||||
changes.remove(at: minIndex)
|
||||
changes.remove(at: maxIndex.advanced(by: -1))
|
||||
changes.insert(.move(move), at: minIndex)
|
||||
}
|
||||
}
|
||||
|
||||
return changes
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
public struct ChangeWithIndexPath {
|
||||
|
||||
public let inserts: [IndexPath]
|
||||
public let deletes: [IndexPath]
|
||||
public let replaces: [IndexPath]
|
||||
public let moves: [(from: IndexPath, to: IndexPath)]
|
||||
|
||||
public init(
|
||||
inserts: [IndexPath],
|
||||
deletes: [IndexPath],
|
||||
replaces:[IndexPath],
|
||||
moves: [(from: IndexPath, to: IndexPath)]) {
|
||||
|
||||
self.inserts = inserts
|
||||
self.deletes = deletes
|
||||
self.replaces = replaces
|
||||
self.moves = moves
|
||||
}
|
||||
}
|
||||
|
||||
public class IndexPathConverter {
|
||||
|
||||
public init() {
|
||||
|
||||
}
|
||||
|
||||
public func convert<T>(changes: [Change<T>], section: Int) -> ChangeWithIndexPath {
|
||||
let inserts = changes.compactMap({ $0.insert }).map({ $0.index.toIndexPath(section: section) })
|
||||
let deletes = changes.compactMap({ $0.delete }).map({ $0.index.toIndexPath(section: section) })
|
||||
let replaces = changes.compactMap({ $0.replace }).map({ $0.index.toIndexPath(section: section) })
|
||||
let moves = changes.compactMap({ $0.move }).map({
|
||||
(
|
||||
from: $0.fromIndex.toIndexPath(section: section),
|
||||
to: $0.toIndex.toIndexPath(section: section)
|
||||
)
|
||||
})
|
||||
|
||||
return ChangeWithIndexPath(
|
||||
inserts: inserts,
|
||||
deletes: deletes,
|
||||
replaces: replaces,
|
||||
moves: moves
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension Int {
|
||||
|
||||
fileprivate func toIndexPath(section: Int) -> IndexPath {
|
||||
return IndexPath(item: self, section: section)
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
public struct ChangeWithIndexSet {
|
||||
|
||||
public let inserts: IndexSet
|
||||
public let deletes: IndexSet
|
||||
public let replaces: IndexSet
|
||||
public let moves: [(from: Int, to: Int)]
|
||||
|
||||
public init(
|
||||
inserts: IndexSet,
|
||||
deletes: IndexSet,
|
||||
replaces: IndexSet,
|
||||
moves: [(from: Int, to: Int)]) {
|
||||
|
||||
self.inserts = inserts
|
||||
self.deletes = deletes
|
||||
self.replaces = replaces
|
||||
self.moves = moves
|
||||
}
|
||||
|
||||
public init<T>(changes: [Change<T>]) {
|
||||
inserts = changes
|
||||
.compactMap { $0.insert?.index }
|
||||
.asIndexSet
|
||||
|
||||
deletes = changes
|
||||
.compactMap { $0.delete?.index }
|
||||
.asIndexSet
|
||||
|
||||
replaces = changes
|
||||
.compactMap { $0.replace?.index }
|
||||
.asIndexSet
|
||||
|
||||
moves = changes
|
||||
.compactMap { $0.move }
|
||||
.map { (from: $0.fromIndex, to: $0.toIndex) }
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
import UIKit
|
||||
|
||||
public extension UICollectionView {
|
||||
|
||||
/// Animate reload in a batch update
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - changes: The changes from diff
|
||||
/// - section: The section that all calculated IndexPath belong
|
||||
/// - completion: Called when operation completes
|
||||
public func reload<T: Hashable>(
|
||||
changes: [Change<T>],
|
||||
section: Int = 0,
|
||||
calledInsideBatch: Bool = false,
|
||||
completion: @escaping (Bool) -> Void = { _ in }) {
|
||||
|
||||
let changesWithIndexPath = IndexPathConverter().convert(changes: changes, section: section)
|
||||
|
||||
if calledInsideBatch {
|
||||
internalBatchUpdates(changesWithIndexPath: changesWithIndexPath)
|
||||
} else {
|
||||
performBatchUpdates({
|
||||
internalBatchUpdates(changesWithIndexPath: changesWithIndexPath)
|
||||
}, completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Animate sections reload in a batch update
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - changes: The changes from diff
|
||||
/// - completion: Called when operation completes
|
||||
public func reloadSections<T: Hashable>(
|
||||
changes: [Change<T>],
|
||||
calledInsideBatch: Bool = false,
|
||||
completion: @escaping (Bool) -> Void = { _ in }) {
|
||||
|
||||
let changesWithIndexSet = ChangeWithIndexSet(changes: changes)
|
||||
|
||||
if calledInsideBatch {
|
||||
internalBatchUpdates(changesWithIndexSet: changesWithIndexSet)
|
||||
} else {
|
||||
performBatchUpdates({
|
||||
internalBatchUpdates(changesWithIndexSet: changesWithIndexSet)
|
||||
}, completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Helper
|
||||
|
||||
private func internalBatchUpdates(changesWithIndexPath: ChangeWithIndexPath) {
|
||||
changesWithIndexPath.deletes.executeIfPresent {
|
||||
deleteItems(at: $0)
|
||||
}
|
||||
|
||||
changesWithIndexPath.inserts.executeIfPresent {
|
||||
insertItems(at: $0)
|
||||
}
|
||||
|
||||
changesWithIndexPath.moves.executeIfPresent {
|
||||
$0.forEach { move in
|
||||
moveItem(at: move.from, to: move.to)
|
||||
}
|
||||
}
|
||||
|
||||
changesWithIndexPath.replaces.executeIfPresent {
|
||||
reloadItems(at: $0)
|
||||
}
|
||||
}
|
||||
|
||||
private func internalBatchUpdates(changesWithIndexSet: ChangeWithIndexSet) {
|
||||
changesWithIndexSet.deletes.executeIfPresent {
|
||||
deleteSections($0)
|
||||
}
|
||||
|
||||
changesWithIndexSet.inserts.executeIfPresent {
|
||||
insertSections($0)
|
||||
}
|
||||
|
||||
changesWithIndexSet.moves.executeIfPresent {
|
||||
$0.forEach { move in
|
||||
moveSection(move.from, toSection: move.to)
|
||||
}
|
||||
}
|
||||
|
||||
changesWithIndexSet.replaces.executeIfPresent {
|
||||
reloadSections($0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
import UIKit
|
||||
|
||||
public extension UITableView {
|
||||
|
||||
/// Animate reload in a batch update
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - changes: The changes from diff
|
||||
/// - section: The section that all calculated IndexPath belong
|
||||
/// - insertionAnimation: The animation for insert rows
|
||||
/// - deletionAnimation: The animation for delete rows
|
||||
/// - replacementAnimation: The animation for reload rows
|
||||
/// - completion: Called when operation completes
|
||||
public func reload<T: Hashable>(
|
||||
changes: [Change<T>],
|
||||
section: Int = 0,
|
||||
insertionAnimation: UITableViewRowAnimation = .automatic,
|
||||
deletionAnimation: UITableViewRowAnimation = .automatic,
|
||||
replacementAnimation: UITableViewRowAnimation = .automatic,
|
||||
completion: @escaping (Bool) -> Void) {
|
||||
|
||||
let changesWithIndexPath = IndexPathConverter().convert(changes: changes, section: section)
|
||||
|
||||
// reloadRows needs to be called outside the batch
|
||||
|
||||
if #available(iOS 11, tvOS 11, *) {
|
||||
performBatchUpdates({
|
||||
internalBatchUpdates(changesWithIndexPath: changesWithIndexPath,
|
||||
insertionAnimation: insertionAnimation,
|
||||
deletionAnimation: deletionAnimation)
|
||||
}, completion: completion)
|
||||
|
||||
changesWithIndexPath.replaces.executeIfPresent {
|
||||
self.reloadRows(at: $0, with: replacementAnimation)
|
||||
}
|
||||
} else {
|
||||
beginUpdates()
|
||||
internalBatchUpdates(changesWithIndexPath: changesWithIndexPath,
|
||||
insertionAnimation: insertionAnimation,
|
||||
deletionAnimation: deletionAnimation)
|
||||
endUpdates()
|
||||
|
||||
changesWithIndexPath.replaces.executeIfPresent {
|
||||
reloadRows(at: $0, with: replacementAnimation)
|
||||
}
|
||||
|
||||
completion(true)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helper
|
||||
|
||||
private func internalBatchUpdates(changesWithIndexPath: ChangeWithIndexPath,
|
||||
insertionAnimation: UITableViewRowAnimation,
|
||||
deletionAnimation: UITableViewRowAnimation) {
|
||||
changesWithIndexPath.deletes.executeIfPresent {
|
||||
deleteRows(at: $0, with: deletionAnimation)
|
||||
}
|
||||
|
||||
changesWithIndexPath.inserts.executeIfPresent {
|
||||
insertRows(at: $0, with: insertionAnimation)
|
||||
}
|
||||
|
||||
changesWithIndexPath.moves.executeIfPresent {
|
||||
$0.forEach { move in
|
||||
moveRow(at: move.from, to: move.to)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,28 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
public enum Math {
|
||||
public static func lerp<T: FloatingPoint>(from: T, to: T, progress: T) -> T {
|
||||
return from + progress * (to - from);
|
||||
}
|
||||
|
||||
public static func lerp(from: UIColor, to: UIColor, progress: CGFloat) -> UIColor {
|
||||
let rgba1 = from.rgba
|
||||
let rgba2 = to.rgba
|
||||
let r = lerp(from: rgba1.r, to: rgba2.r, progress: progress)
|
||||
let g = lerp(from: rgba1.g, to: rgba2.g, progress: progress)
|
||||
let b = lerp(from: rgba1.b, to: rgba2.b, progress: progress)
|
||||
let a = lerp(from: rgba1.a, to: rgba2.a, progress: progress)
|
||||
return UIColor(red: r, green: g, blue: b, alpha: a)
|
||||
}
|
||||
|
||||
public static func lerp(from: CGRect, to: CGRect, progress: CGFloat) -> CGRect {
|
||||
let x = lerp(from: from.origin.x, to: to.origin.x, progress: progress)
|
||||
let y = lerp(from: from.origin.y, to: to.origin.y, progress: progress)
|
||||
let w = lerp(from: from.size.width, to: to.size.width, progress: progress)
|
||||
let h = lerp(from: from.size.height, to: to.size.height, progress: progress)
|
||||
return CGRect(x: x, y: y, width: w, height: h)
|
||||
}
|
||||
}
|
||||
|
||||
public enum Progress {
|
||||
@@ -34,3 +53,17 @@ public enum Progress {
|
||||
return centeredProgress
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public enum Inertia {
|
||||
|
||||
public static func applyResistance(for source: CGFloat, with scrollPosition: CGFloat, decelerationRate: UIScrollView.DecelerationRate = .fast, maximumScrollDistance: CGFloat = 120) -> CGFloat {
|
||||
let resistantDistance = (decelerationRate.rawValue * abs(scrollPosition) * maximumScrollDistance) / (maximumScrollDistance + decelerationRate.rawValue * abs(scrollPosition))
|
||||
return source + (scrollPosition < 0 ? -resistantDistance : resistantDistance)
|
||||
}
|
||||
|
||||
// Distance travelled after deceleration to zero velocity at a constant rate
|
||||
public static func project(initialVelocity: CGFloat, decelerationRate: UIScrollView.DecelerationRate = .fast) -> CGFloat {
|
||||
return (initialVelocity / 1000.0) * decelerationRate.rawValue / (1.0 - decelerationRate.rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import UIKit
|
||||
|
||||
extension NSMutableAttributedString {
|
||||
|
||||
public func addAttributes(for string: String, attributes: [NSAttributedStringKey : Any]) -> Self {
|
||||
public func addAttributes(for string: String, attributes: [NSAttributedString.Key : Any]) -> Self {
|
||||
let range = (self.string as NSString).range(of: string)
|
||||
addAttributes(attributes, range: range)
|
||||
return self
|
||||
@@ -12,9 +12,9 @@ extension NSMutableAttributedString {
|
||||
public func setAsLink(textToFind:String, linkURL:String, color: UIColor, font: UIFont? = nil) -> Self {
|
||||
let foundRange = self.mutableString.range(of: textToFind)
|
||||
if foundRange.location != NSNotFound {
|
||||
self.addAttribute(NSAttributedStringKey.link, value: linkURL, range: foundRange)
|
||||
self.addAttribute(NSAttributedStringKey.foregroundColor, value: color, range: foundRange)
|
||||
if let font = font { self.addAttribute(NSAttributedStringKey.font, value: font, range: foundRange) }
|
||||
self.addAttribute(NSAttributedString.Key.link, value: linkURL, range: foundRange)
|
||||
self.addAttribute(NSAttributedString.Key.foregroundColor, value: color, range: foundRange)
|
||||
if let font = font { self.addAttribute(NSAttributedString.Key.font, value: font, range: foundRange) }
|
||||
}
|
||||
return self
|
||||
}
|
||||
@@ -28,11 +28,4 @@ extension NSAttributedString {
|
||||
let result = boundingRect(with: size, options: [ .usesLineFragmentOrigin, .usesFontLeading ], context: nil).size
|
||||
return result
|
||||
}
|
||||
|
||||
// public static func + (left: NSAttributedString, right: NSAttributedString) -> NSAttributedString {
|
||||
// return (left.mutableCopy() as! NSMutableAttributedString).then {
|
||||
// $0.append(right)
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -17,13 +17,13 @@ extension CAMediaTimingFunction {
|
||||
public static func timingFunction(withCurve curve: TimingFunction) -> CAMediaTimingFunction {
|
||||
switch curve {
|
||||
case .linear:
|
||||
return CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
|
||||
return CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
|
||||
case .easeIn:
|
||||
return CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
|
||||
return CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeIn)
|
||||
case .easeOut:
|
||||
return CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
|
||||
return CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut)
|
||||
case .easeInOut:
|
||||
return CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
|
||||
return CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
|
||||
case .spring:
|
||||
return CAMediaTimingFunction(controlPoints: 0.5, 1.1 + Float(1/3), 1, 1)
|
||||
case .easeInSine:
|
||||
|
||||
@@ -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