├── .gitignore ├── APReorderableStackView.podspec ├── LICENSE ├── README.md ├── ReorderStackView.xcodeproj ├── project.pbxproj └── project.xcworkspace │ └── contents.xcworkspacedata ├── ReorderStackView ├── APRedorderableStackView.swift ├── AppDelegate.swift ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json ├── Base.lproj │ └── LaunchScreen.storyboard ├── ExampleView.swift ├── ExampleViewController.swift └── Info.plist └── UIColorExtension.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | build/ 4 | *.pbxuser 5 | !default.pbxuser 6 | *.mode1v3 7 | !default.mode1v3 8 | *.mode2v3 9 | !default.mode2v3 10 | *.perspectivev3 11 | !default.perspectivev3 12 | xcuserdata 13 | *.xccheckout 14 | *.moved-aside 15 | DerivedData 16 | *.hmap 17 | *.ipa 18 | *.xcuserstate 19 | 20 | xcshareddata 21 | 22 | # CocoaPods 23 | # 24 | # We recommend against adding the Pods directory to your .gitignore. However 25 | # you should judge for yourself, the pros and cons are mentioned at: 26 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control 27 | # 28 | # Pods/ 29 | 30 | # Carthage 31 | # 32 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 33 | # Carthage/Checkouts 34 | 35 | Carthage/Build 36 | -------------------------------------------------------------------------------- /APReorderableStackView.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |spec| 2 | spec.name = 'APReorderableStackView' 3 | spec.version = '1.0' 4 | spec.license = { :type => 'MIT', :file => 'LICENSE' } 5 | spec.homepage = 'https://github.com/clayellis/APReorderableStackView' 6 | spec.authors = ['Clay Ellis'] 7 | spec.summary = 'A UIStackView with drag to reorder support' 8 | spec.source = { :git => 'https://github.com/clayellis/APReorderableStackView.git', :tag => 'v1.0' } 9 | spec.source_files = 'ReorderStackView/APRedorderableStackView.swift' 10 | spec.swift_version = '5.0' 11 | spec.platforms = { "ios" => "9.0" } 12 | end 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Clay Ellis and contributors 4 | See the full list at https://github.com/clayellis/APReorderableStackView/contributors 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # APReorderableStackView 2 | A demo project showing off an extension to UIStackView that provides a drag to reorder behavior for Appsidian's APKit 3 | -------------------------------------------------------------------------------- /ReorderStackView.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 6F7D74CA1BD1B46B0035E6E3 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7D74C91BD1B46B0035E6E3 /* AppDelegate.swift */; }; 11 | 6F7D74CC1BD1B46B0035E6E3 /* ExampleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7D74CB1BD1B46B0035E6E3 /* ExampleViewController.swift */; }; 12 | 6F7D74CE1BD1B46B0035E6E3 /* ExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7D74CD1BD1B46B0035E6E3 /* ExampleView.swift */; }; 13 | 6F7D74D41BD1B46B0035E6E3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6F7D74D31BD1B46B0035E6E3 /* Assets.xcassets */; }; 14 | 6F7D74D71BD1B46B0035E6E3 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6F7D74D51BD1B46B0035E6E3 /* LaunchScreen.storyboard */; }; 15 | 6F7D74DE1BD1B6740035E6E3 /* APRedorderableStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7D74DD1BD1B6740035E6E3 /* APRedorderableStackView.swift */; }; 16 | 6F7D74E41BD1B8370035E6E3 /* UIColorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7D74E31BD1B8370035E6E3 /* UIColorExtension.swift */; }; 17 | /* End PBXBuildFile section */ 18 | 19 | /* Begin PBXFileReference section */ 20 | 6F7D74C61BD1B46B0035E6E3 /* ReorderStackView.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ReorderStackView.app; sourceTree = BUILT_PRODUCTS_DIR; }; 21 | 6F7D74C91BD1B46B0035E6E3 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 22 | 6F7D74CB1BD1B46B0035E6E3 /* ExampleViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleViewController.swift; sourceTree = ""; }; 23 | 6F7D74CD1BD1B46B0035E6E3 /* ExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleView.swift; sourceTree = ""; }; 24 | 6F7D74D11BD1B46B0035E6E3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 25 | 6F7D74D31BD1B46B0035E6E3 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 26 | 6F7D74D61BD1B46B0035E6E3 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 27 | 6F7D74DD1BD1B6740035E6E3 /* APRedorderableStackView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APRedorderableStackView.swift; sourceTree = ""; }; 28 | 6F7D74E31BD1B8370035E6E3 /* UIColorExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = UIColorExtension.swift; path = ../UIColorExtension.swift; sourceTree = ""; }; 29 | /* End PBXFileReference section */ 30 | 31 | /* Begin PBXFrameworksBuildPhase section */ 32 | 6F7D74C31BD1B46B0035E6E3 /* Frameworks */ = { 33 | isa = PBXFrameworksBuildPhase; 34 | buildActionMask = 2147483647; 35 | files = ( 36 | ); 37 | runOnlyForDeploymentPostprocessing = 0; 38 | }; 39 | /* End PBXFrameworksBuildPhase section */ 40 | 41 | /* Begin PBXGroup section */ 42 | 6F7D74BD1BD1B46B0035E6E3 = { 43 | isa = PBXGroup; 44 | children = ( 45 | 6F7D74C81BD1B46B0035E6E3 /* ReorderStackView */, 46 | 6F7D74C71BD1B46B0035E6E3 /* Products */, 47 | ); 48 | sourceTree = ""; 49 | }; 50 | 6F7D74C71BD1B46B0035E6E3 /* Products */ = { 51 | isa = PBXGroup; 52 | children = ( 53 | 6F7D74C61BD1B46B0035E6E3 /* ReorderStackView.app */, 54 | ); 55 | name = Products; 56 | sourceTree = ""; 57 | }; 58 | 6F7D74C81BD1B46B0035E6E3 /* ReorderStackView */ = { 59 | isa = PBXGroup; 60 | children = ( 61 | 6F7D74C91BD1B46B0035E6E3 /* AppDelegate.swift */, 62 | 6F7D74CB1BD1B46B0035E6E3 /* ExampleViewController.swift */, 63 | 6F7D74CD1BD1B46B0035E6E3 /* ExampleView.swift */, 64 | 6F7D74DD1BD1B6740035E6E3 /* APRedorderableStackView.swift */, 65 | 6F7D74E31BD1B8370035E6E3 /* UIColorExtension.swift */, 66 | 6F7D74D11BD1B46B0035E6E3 /* Info.plist */, 67 | 6F7D74D31BD1B46B0035E6E3 /* Assets.xcassets */, 68 | 6F7D74D51BD1B46B0035E6E3 /* LaunchScreen.storyboard */, 69 | ); 70 | path = ReorderStackView; 71 | sourceTree = ""; 72 | }; 73 | /* End PBXGroup section */ 74 | 75 | /* Begin PBXNativeTarget section */ 76 | 6F7D74C51BD1B46B0035E6E3 /* ReorderStackView */ = { 77 | isa = PBXNativeTarget; 78 | buildConfigurationList = 6F7D74DA1BD1B46B0035E6E3 /* Build configuration list for PBXNativeTarget "ReorderStackView" */; 79 | buildPhases = ( 80 | 6F7D74C21BD1B46B0035E6E3 /* Sources */, 81 | 6F7D74C31BD1B46B0035E6E3 /* Frameworks */, 82 | 6F7D74C41BD1B46B0035E6E3 /* Resources */, 83 | ); 84 | buildRules = ( 85 | ); 86 | dependencies = ( 87 | ); 88 | name = ReorderStackView; 89 | productName = ReorderStackView; 90 | productReference = 6F7D74C61BD1B46B0035E6E3 /* ReorderStackView.app */; 91 | productType = "com.apple.product-type.application"; 92 | }; 93 | /* End PBXNativeTarget section */ 94 | 95 | /* Begin PBXProject section */ 96 | 6F7D74BE1BD1B46B0035E6E3 /* Project object */ = { 97 | isa = PBXProject; 98 | attributes = { 99 | LastUpgradeCheck = 0710; 100 | ORGANIZATIONNAME = "Clay Ellis"; 101 | TargetAttributes = { 102 | 6F7D74C51BD1B46B0035E6E3 = { 103 | CreatedOnToolsVersion = 7.0; 104 | LastSwiftMigration = 0820; 105 | }; 106 | }; 107 | }; 108 | buildConfigurationList = 6F7D74C11BD1B46B0035E6E3 /* Build configuration list for PBXProject "ReorderStackView" */; 109 | compatibilityVersion = "Xcode 3.2"; 110 | developmentRegion = English; 111 | hasScannedForEncodings = 0; 112 | knownRegions = ( 113 | English, 114 | en, 115 | Base, 116 | ); 117 | mainGroup = 6F7D74BD1BD1B46B0035E6E3; 118 | productRefGroup = 6F7D74C71BD1B46B0035E6E3 /* Products */; 119 | projectDirPath = ""; 120 | projectRoot = ""; 121 | targets = ( 122 | 6F7D74C51BD1B46B0035E6E3 /* ReorderStackView */, 123 | ); 124 | }; 125 | /* End PBXProject section */ 126 | 127 | /* Begin PBXResourcesBuildPhase section */ 128 | 6F7D74C41BD1B46B0035E6E3 /* Resources */ = { 129 | isa = PBXResourcesBuildPhase; 130 | buildActionMask = 2147483647; 131 | files = ( 132 | 6F7D74D71BD1B46B0035E6E3 /* LaunchScreen.storyboard in Resources */, 133 | 6F7D74D41BD1B46B0035E6E3 /* Assets.xcassets in Resources */, 134 | ); 135 | runOnlyForDeploymentPostprocessing = 0; 136 | }; 137 | /* End PBXResourcesBuildPhase section */ 138 | 139 | /* Begin PBXSourcesBuildPhase section */ 140 | 6F7D74C21BD1B46B0035E6E3 /* Sources */ = { 141 | isa = PBXSourcesBuildPhase; 142 | buildActionMask = 2147483647; 143 | files = ( 144 | 6F7D74CC1BD1B46B0035E6E3 /* ExampleViewController.swift in Sources */, 145 | 6F7D74CA1BD1B46B0035E6E3 /* AppDelegate.swift in Sources */, 146 | 6F7D74CE1BD1B46B0035E6E3 /* ExampleView.swift in Sources */, 147 | 6F7D74DE1BD1B6740035E6E3 /* APRedorderableStackView.swift in Sources */, 148 | 6F7D74E41BD1B8370035E6E3 /* UIColorExtension.swift in Sources */, 149 | ); 150 | runOnlyForDeploymentPostprocessing = 0; 151 | }; 152 | /* End PBXSourcesBuildPhase section */ 153 | 154 | /* Begin PBXVariantGroup section */ 155 | 6F7D74D51BD1B46B0035E6E3 /* LaunchScreen.storyboard */ = { 156 | isa = PBXVariantGroup; 157 | children = ( 158 | 6F7D74D61BD1B46B0035E6E3 /* Base */, 159 | ); 160 | name = LaunchScreen.storyboard; 161 | sourceTree = ""; 162 | }; 163 | /* End PBXVariantGroup section */ 164 | 165 | /* Begin XCBuildConfiguration section */ 166 | 6F7D74D81BD1B46B0035E6E3 /* Debug */ = { 167 | isa = XCBuildConfiguration; 168 | buildSettings = { 169 | ALWAYS_SEARCH_USER_PATHS = NO; 170 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 171 | CLANG_CXX_LIBRARY = "libc++"; 172 | CLANG_ENABLE_MODULES = YES; 173 | CLANG_ENABLE_OBJC_ARC = YES; 174 | CLANG_WARN_BOOL_CONVERSION = YES; 175 | CLANG_WARN_CONSTANT_CONVERSION = YES; 176 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 177 | CLANG_WARN_EMPTY_BODY = YES; 178 | CLANG_WARN_ENUM_CONVERSION = YES; 179 | CLANG_WARN_INT_CONVERSION = YES; 180 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 181 | CLANG_WARN_UNREACHABLE_CODE = YES; 182 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 183 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 184 | COPY_PHASE_STRIP = NO; 185 | DEBUG_INFORMATION_FORMAT = dwarf; 186 | ENABLE_STRICT_OBJC_MSGSEND = YES; 187 | ENABLE_TESTABILITY = YES; 188 | GCC_C_LANGUAGE_STANDARD = gnu99; 189 | GCC_DYNAMIC_NO_PIC = NO; 190 | GCC_NO_COMMON_BLOCKS = YES; 191 | GCC_OPTIMIZATION_LEVEL = 0; 192 | GCC_PREPROCESSOR_DEFINITIONS = ( 193 | "DEBUG=1", 194 | "$(inherited)", 195 | ); 196 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 197 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 198 | GCC_WARN_UNDECLARED_SELECTOR = YES; 199 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 200 | GCC_WARN_UNUSED_FUNCTION = YES; 201 | GCC_WARN_UNUSED_VARIABLE = YES; 202 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 203 | MTL_ENABLE_DEBUG_INFO = YES; 204 | ONLY_ACTIVE_ARCH = YES; 205 | SDKROOT = iphoneos; 206 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 207 | }; 208 | name = Debug; 209 | }; 210 | 6F7D74D91BD1B46B0035E6E3 /* Release */ = { 211 | isa = XCBuildConfiguration; 212 | buildSettings = { 213 | ALWAYS_SEARCH_USER_PATHS = NO; 214 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 215 | CLANG_CXX_LIBRARY = "libc++"; 216 | CLANG_ENABLE_MODULES = YES; 217 | CLANG_ENABLE_OBJC_ARC = YES; 218 | CLANG_WARN_BOOL_CONVERSION = YES; 219 | CLANG_WARN_CONSTANT_CONVERSION = YES; 220 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 221 | CLANG_WARN_EMPTY_BODY = YES; 222 | CLANG_WARN_ENUM_CONVERSION = YES; 223 | CLANG_WARN_INT_CONVERSION = YES; 224 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 225 | CLANG_WARN_UNREACHABLE_CODE = YES; 226 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 227 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 228 | COPY_PHASE_STRIP = NO; 229 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 230 | ENABLE_NS_ASSERTIONS = NO; 231 | ENABLE_STRICT_OBJC_MSGSEND = YES; 232 | GCC_C_LANGUAGE_STANDARD = gnu99; 233 | GCC_NO_COMMON_BLOCKS = YES; 234 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 235 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 236 | GCC_WARN_UNDECLARED_SELECTOR = YES; 237 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 238 | GCC_WARN_UNUSED_FUNCTION = YES; 239 | GCC_WARN_UNUSED_VARIABLE = YES; 240 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 241 | MTL_ENABLE_DEBUG_INFO = NO; 242 | SDKROOT = iphoneos; 243 | VALIDATE_PRODUCT = YES; 244 | }; 245 | name = Release; 246 | }; 247 | 6F7D74DB1BD1B46B0035E6E3 /* Debug */ = { 248 | isa = XCBuildConfiguration; 249 | buildSettings = { 250 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 251 | FRAMEWORK_SEARCH_PATHS = ""; 252 | INFOPLIST_FILE = ReorderStackView/Info.plist; 253 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 254 | PRODUCT_BUNDLE_IDENTIFIER = clayellis.ReorderStackView; 255 | PRODUCT_NAME = "$(TARGET_NAME)"; 256 | SWIFT_VERSION = 5.0; 257 | }; 258 | name = Debug; 259 | }; 260 | 6F7D74DC1BD1B46B0035E6E3 /* Release */ = { 261 | isa = XCBuildConfiguration; 262 | buildSettings = { 263 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 264 | FRAMEWORK_SEARCH_PATHS = ""; 265 | INFOPLIST_FILE = ReorderStackView/Info.plist; 266 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 267 | PRODUCT_BUNDLE_IDENTIFIER = clayellis.ReorderStackView; 268 | PRODUCT_NAME = "$(TARGET_NAME)"; 269 | SWIFT_VERSION = 5.0; 270 | }; 271 | name = Release; 272 | }; 273 | /* End XCBuildConfiguration section */ 274 | 275 | /* Begin XCConfigurationList section */ 276 | 6F7D74C11BD1B46B0035E6E3 /* Build configuration list for PBXProject "ReorderStackView" */ = { 277 | isa = XCConfigurationList; 278 | buildConfigurations = ( 279 | 6F7D74D81BD1B46B0035E6E3 /* Debug */, 280 | 6F7D74D91BD1B46B0035E6E3 /* Release */, 281 | ); 282 | defaultConfigurationIsVisible = 0; 283 | defaultConfigurationName = Release; 284 | }; 285 | 6F7D74DA1BD1B46B0035E6E3 /* Build configuration list for PBXNativeTarget "ReorderStackView" */ = { 286 | isa = XCConfigurationList; 287 | buildConfigurations = ( 288 | 6F7D74DB1BD1B46B0035E6E3 /* Debug */, 289 | 6F7D74DC1BD1B46B0035E6E3 /* Release */, 290 | ); 291 | defaultConfigurationIsVisible = 0; 292 | defaultConfigurationName = Release; 293 | }; 294 | /* End XCConfigurationList section */ 295 | }; 296 | rootObject = 6F7D74BE1BD1B46B0035E6E3 /* Project object */; 297 | } 298 | -------------------------------------------------------------------------------- /ReorderStackView.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ReorderStackView/APRedorderableStackView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APRedorderableStackView.swift 3 | // ReorderStackView 4 | // 5 | // Created by Clay Ellis on 10/16/15. 6 | // Copyright © 2015 Appsidian. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @objc 12 | public protocol APStackViewReorderDelegate { 13 | /// didBeginReordering - called when reordering begins 14 | @objc optional func didBeginReordering() 15 | 16 | /// Whenever a user drags a subview for a reordering, the delegate is told whether the direction 17 | /// was up or down, as well as what the max and min Y values are of the subview 18 | @objc optional func didDragToReorder(inUpDirection up: Bool, maxY: CGFloat, minY: CGFloat) 19 | 20 | /// didReorder - called whenever a subview was reordered (returns the new index) 21 | 22 | /// didEndReordering - called when reordering ends 23 | @objc optional func didEndReordering() 24 | } 25 | 26 | public class APRedorderableStackView: UIStackView, UIGestureRecognizerDelegate { 27 | 28 | /// Setting `reorderdingEnabled` to `true` enables a drag to reorder behavior like `UITableView` 29 | public var reorderingEnabled = false { 30 | didSet { 31 | self.setReorderingEnabled(self.reorderingEnabled) 32 | } 33 | } 34 | public var reorderDelegate: APStackViewReorderDelegate? 35 | 36 | // Gesture recognizers 37 | fileprivate var longPressGRS = [UILongPressGestureRecognizer]() 38 | 39 | // Views for reordering 40 | fileprivate var temporaryView: UIView! 41 | fileprivate var temporaryViewForShadow: UIView! 42 | fileprivate var actualView: UIView! 43 | 44 | // Values for reordering 45 | fileprivate var reordering = false 46 | fileprivate var finalReorderFrame: CGRect! 47 | fileprivate var originalPosition: CGPoint! 48 | fileprivate var pointForReordering: CGPoint! 49 | 50 | // Appearance Constants 51 | public var clipsToBoundsWhileReordering = false 52 | public var cornerRadii: CGFloat = 5 53 | public var temporaryViewScale: CGFloat = 1.05 54 | public var otherViewsScale: CGFloat = 0.97 55 | public var temporaryViewAlpha: CGFloat = 0.9 56 | /// The gap created once the long press drag is triggered 57 | public var dragHintSpacing: CGFloat = 5 58 | public var longPressMinimumPressDuration = 0.2 { 59 | didSet { 60 | self.updateMinimumPressDuration() 61 | } 62 | } 63 | 64 | // MARK:- Reordering Methods 65 | // --------------------------------------------------------------------------------------------- 66 | override public func addArrangedSubview(_ view: UIView) { 67 | super.addArrangedSubview(view) 68 | self.addLongPressGestureRecognizerForReorderingToView(view) 69 | } 70 | 71 | fileprivate func addLongPressGestureRecognizerForReorderingToView(_ view: UIView) { 72 | let longPressGR = UILongPressGestureRecognizer(target: self, action: #selector(APRedorderableStackView.handleLongPress(_:))) 73 | longPressGR.delegate = self 74 | longPressGR.minimumPressDuration = self.longPressMinimumPressDuration 75 | longPressGR.isEnabled = self.reorderingEnabled 76 | view.addGestureRecognizer(longPressGR) 77 | 78 | self.longPressGRS.append(longPressGR) 79 | } 80 | 81 | fileprivate func setReorderingEnabled(_ enabled: Bool) { 82 | for longPressGR in self.longPressGRS { 83 | longPressGR.isEnabled = enabled 84 | } 85 | } 86 | 87 | fileprivate func updateMinimumPressDuration() { 88 | for longPressGR in self.longPressGRS { 89 | longPressGR.minimumPressDuration = self.longPressMinimumPressDuration 90 | } 91 | } 92 | 93 | @objc internal func handleLongPress(_ gr: UILongPressGestureRecognizer) { 94 | 95 | if gr.state == .began { 96 | 97 | self.reordering = true 98 | self.reorderDelegate?.didBeginReordering?() 99 | 100 | self.actualView = gr.view! 101 | self.originalPosition = gr.location(in: self) 102 | self.originalPosition.y -= self.dragHintSpacing 103 | self.pointForReordering = self.originalPosition 104 | self.prepareForReordering() 105 | 106 | } else if gr.state == .changed { 107 | 108 | // Drag the temporaryView 109 | let newLocation = gr.location(in: self) 110 | let xOffset = newLocation.x - originalPosition.x 111 | let yOffset = newLocation.y - originalPosition.y 112 | let translation = CGAffineTransform(translationX: xOffset, y: yOffset) 113 | // Replicate the scale that was initially applied in perpareForReordering: 114 | let scale = CGAffineTransform(scaleX: self.temporaryViewScale, y: self.temporaryViewScale) 115 | self.temporaryView.transform = scale.concatenating(translation) 116 | self.temporaryViewForShadow.transform = translation 117 | 118 | // Use the midY of the temporaryView to determine the dragging direction, location 119 | // maxY and minY are used in the delegate call didDragToReorder 120 | let maxY = self.temporaryView.frame.maxY 121 | let midY = self.temporaryView.frame.midY 122 | let minY = self.temporaryView.frame.minY 123 | let index = self.indexOfArrangedSubview(self.actualView) 124 | 125 | if midY > self.pointForReordering.y { 126 | // Dragging the view down 127 | self.reorderDelegate?.didDragToReorder?(inUpDirection: false, maxY: maxY, minY: minY) 128 | 129 | if let nextView = self.getNextViewInStack(usingIndex: index) { 130 | if midY > nextView.frame.midY { 131 | 132 | // Swap the two arranged subviews 133 | UIView.animate(withDuration: 0.2, animations: { 134 | self.insertArrangedSubview(nextView, at: index) 135 | self.insertArrangedSubview(self.actualView, at: index + 1) 136 | }) 137 | self.finalReorderFrame = self.actualView.frame 138 | self.pointForReordering.y = self.actualView.frame.midY 139 | } 140 | } 141 | 142 | } else { 143 | // Dragging the view up 144 | self.reorderDelegate?.didDragToReorder?(inUpDirection: true, maxY: maxY, minY: minY) 145 | 146 | if let previousView = self.getPreviousViewInStack(usingIndex: index) { 147 | if midY < previousView.frame.midY { 148 | 149 | // Swap the two arranged subviews 150 | UIView.animate(withDuration: 0.2, animations: { 151 | self.insertArrangedSubview(previousView, at: index) 152 | self.insertArrangedSubview(self.actualView, at: index - 1) 153 | }) 154 | self.finalReorderFrame = self.actualView.frame 155 | self.pointForReordering.y = self.actualView.frame.midY 156 | 157 | } 158 | } 159 | } 160 | 161 | } else if gr.state == .ended || gr.state == .cancelled || gr.state == .failed { 162 | 163 | self.cleanupUpAfterReordering() 164 | self.reordering = false 165 | self.reorderDelegate?.didEndReordering?() 166 | } 167 | 168 | } 169 | 170 | fileprivate func prepareForReordering() { 171 | 172 | self.clipsToBounds = self.clipsToBoundsWhileReordering 173 | 174 | // Configure the temporary view 175 | self.temporaryView = self.actualView.snapshotView(afterScreenUpdates: true) 176 | self.temporaryView.frame = self.actualView.frame 177 | self.finalReorderFrame = self.actualView.frame 178 | self.addSubview(self.temporaryView) 179 | 180 | // Hide the actual view and grow the temporaryView 181 | self.actualView.alpha = 0 182 | 183 | UIView.animate(withDuration: 0.4, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0, options: [.allowUserInteraction, .beginFromCurrentState], animations: { 184 | 185 | self.styleViewsForReordering() 186 | 187 | }, completion: nil) 188 | } 189 | 190 | fileprivate func cleanupUpAfterReordering() { 191 | 192 | UIView.animate(withDuration: 0.4, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0, options: [.allowUserInteraction, .beginFromCurrentState], animations: { 193 | 194 | self.styleViewsForEndReordering() 195 | 196 | }, completion: { (Bool) -> Void in 197 | // Hide the temporaryView, show the actualView 198 | self.temporaryViewForShadow.removeFromSuperview() 199 | self.temporaryView.removeFromSuperview() 200 | self.actualView.alpha = 1 201 | self.clipsToBounds = !self.clipsToBoundsWhileReordering 202 | }) 203 | 204 | } 205 | 206 | 207 | // MARK:- View Styling Methods 208 | // --------------------------------------------------------------------------------------------- 209 | 210 | fileprivate func styleViewsForReordering() { 211 | 212 | let roundKey = "Round" 213 | let round = CABasicAnimation(keyPath: "cornerRadius") 214 | round.fromValue = 0 215 | round.toValue = self.cornerRadii 216 | round.duration = 0.1 217 | round.isRemovedOnCompletion = false 218 | round.fillMode = CAMediaTimingFillMode.forwards 219 | 220 | // Grow, hint with offset, fade, round the temporaryView 221 | let scale = CGAffineTransform(scaleX: self.temporaryViewScale, y: self.temporaryViewScale) 222 | let translation = CGAffineTransform(translationX: 0, y: self.dragHintSpacing) 223 | self.temporaryView.transform = scale.concatenating(translation) 224 | self.temporaryView.alpha = self.temporaryViewAlpha 225 | self.temporaryView.layer.add(round, forKey: roundKey) 226 | self.temporaryView.clipsToBounds = true // Clips to bounds to apply corner radius 227 | 228 | // Shadow 229 | self.temporaryViewForShadow = UIView(frame: self.temporaryView.frame) 230 | self.insertSubview(self.temporaryViewForShadow, belowSubview: self.temporaryView) 231 | self.temporaryViewForShadow.layer.shadowColor = UIColor.black.cgColor 232 | self.temporaryViewForShadow.layer.shadowPath = UIBezierPath(roundedRect: self.temporaryView.bounds, cornerRadius: self.cornerRadii).cgPath 233 | 234 | // Shadow animations 235 | let shadowOpacityKey = "ShadowOpacity" 236 | let shadowOpacity = CABasicAnimation(keyPath: "shadowOpacity") 237 | shadowOpacity.fromValue = 0 238 | shadowOpacity.toValue = 0.2 239 | shadowOpacity.duration = 0.2 240 | shadowOpacity.isRemovedOnCompletion = false 241 | shadowOpacity.fillMode = CAMediaTimingFillMode.forwards 242 | 243 | let shadowOffsetKey = "ShadowOffset" 244 | let shadowOffset = CABasicAnimation(keyPath: "shadowOffset.height") 245 | shadowOffset.fromValue = 0 246 | shadowOffset.toValue = 50 247 | shadowOffset.duration = 0.2 248 | shadowOffset.isRemovedOnCompletion = false 249 | shadowOffset.fillMode = CAMediaTimingFillMode.forwards 250 | 251 | let shadowRadiusKey = "ShadowRadius" 252 | let shadowRadius = CABasicAnimation(keyPath: "shadowRadius") 253 | shadowRadius.fromValue = 0 254 | shadowRadius.toValue = 20 255 | shadowRadius.duration = 0.2 256 | shadowRadius.isRemovedOnCompletion = false 257 | shadowRadius.fillMode = CAMediaTimingFillMode.forwards 258 | 259 | self.temporaryViewForShadow.layer.add(shadowOpacity, forKey: shadowOpacityKey) 260 | self.temporaryViewForShadow.layer.add(shadowOffset, forKey: shadowOffsetKey) 261 | self.temporaryViewForShadow.layer.add(shadowRadius, forKey: shadowRadiusKey) 262 | 263 | // Scale down and round other arranged subviews 264 | for subview in self.arrangedSubviews { 265 | if subview != self.actualView { 266 | subview.layer.add(round, forKey: roundKey) 267 | subview.transform = CGAffineTransform(scaleX: self.otherViewsScale, y: self.otherViewsScale) 268 | } 269 | } 270 | } 271 | 272 | fileprivate func styleViewsForEndReordering() { 273 | 274 | let squareKey = "Square" 275 | let square = CABasicAnimation(keyPath: "cornerRadius") 276 | square.fromValue = self.cornerRadii 277 | square.toValue = 0 278 | square.duration = 0.1 279 | square.isRemovedOnCompletion = false 280 | square.fillMode = CAMediaTimingFillMode.forwards 281 | 282 | // Return drag view to original appearance 283 | self.temporaryView.transform = CGAffineTransform(scaleX: 1.0, y: 1.0) 284 | self.temporaryView.frame = self.finalReorderFrame 285 | self.temporaryView.alpha = 1.0 286 | self.temporaryView.layer.add(square, forKey: squareKey) 287 | 288 | // Shadow animations 289 | let shadowOpacityKey = "ShadowOpacity" 290 | let shadowOpacity = CABasicAnimation(keyPath: "shadowOpacity") 291 | shadowOpacity.fromValue = 0.2 292 | shadowOpacity.toValue = 0 293 | shadowOpacity.duration = 0.2 294 | shadowOpacity.isRemovedOnCompletion = false 295 | shadowOpacity.fillMode = CAMediaTimingFillMode.forwards 296 | 297 | let shadowOffsetKey = "ShadowOffset" 298 | let shadowOffset = CABasicAnimation(keyPath: "shadowOffset.height") 299 | shadowOffset.fromValue = 50 300 | shadowOffset.toValue = 0 301 | shadowOffset.duration = 0.2 302 | shadowOffset.isRemovedOnCompletion = false 303 | shadowOffset.fillMode = CAMediaTimingFillMode.forwards 304 | 305 | let shadowRadiusKey = "ShadowRadius" 306 | let shadowRadius = CABasicAnimation(keyPath: "shadowRadius") 307 | shadowRadius.fromValue = 20 308 | shadowRadius.toValue = 0 309 | shadowRadius.duration = 0.4 310 | shadowRadius.isRemovedOnCompletion = false 311 | shadowRadius.fillMode = CAMediaTimingFillMode.forwards 312 | 313 | self.temporaryViewForShadow.layer.add(shadowOpacity, forKey: shadowOpacityKey) 314 | self.temporaryViewForShadow.layer.add(shadowOffset, forKey: shadowOffsetKey) 315 | self.temporaryViewForShadow.layer.add(shadowRadius, forKey: shadowRadiusKey) 316 | 317 | // Return other arranged subviews to original appearances 318 | for subview in self.arrangedSubviews { 319 | UIView.animate(withDuration: 0.3, animations: { 320 | subview.layer.add(square, forKey: squareKey) 321 | subview.transform = CGAffineTransform(scaleX: 1.0, y: 1.0) 322 | }) 323 | } 324 | } 325 | 326 | 327 | // MARK:- Stack View Helper Methods 328 | // --------------------------------------------------------------------------------------------- 329 | 330 | fileprivate func indexOfArrangedSubview(_ view: UIView) -> Int { 331 | for (index, subview) in self.arrangedSubviews.enumerated() { 332 | if view == subview { 333 | return index 334 | } 335 | } 336 | return 0 337 | } 338 | 339 | fileprivate func getPreviousViewInStack(usingIndex index: Int) -> UIView? { 340 | if index == 0 { return nil } 341 | return self.arrangedSubviews[index - 1] 342 | } 343 | 344 | fileprivate func getNextViewInStack(usingIndex index: Int) -> UIView? { 345 | if index == self.arrangedSubviews.count - 1 { return nil } 346 | return self.arrangedSubviews[index + 1] 347 | } 348 | 349 | override public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { 350 | return !self.reordering 351 | } 352 | 353 | } 354 | -------------------------------------------------------------------------------- /ReorderStackView/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // ReorderStackView 4 | // 5 | // Created by Clay Ellis on 10/16/15. 6 | // Copyright © 2015 Clay Ellis. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 18 | 19 | self.window = UIWindow(frame: UIScreen.main.bounds) 20 | let exampleViewController = ExampleViewController() 21 | self.window!.rootViewController = exampleViewController 22 | self.window!.makeKeyAndVisible() 23 | 24 | return true 25 | } 26 | 27 | func applicationWillResignActive(_ application: UIApplication) { 28 | // 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. 29 | // 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. 30 | } 31 | 32 | func applicationDidEnterBackground(_ application: UIApplication) { 33 | // 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. 34 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 35 | } 36 | 37 | func applicationWillEnterForeground(_ application: UIApplication) { 38 | // 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. 39 | } 40 | 41 | func applicationDidBecomeActive(_ application: UIApplication) { 42 | // 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. 43 | } 44 | 45 | func applicationWillTerminate(_ application: UIApplication) { 46 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 47 | } 48 | 49 | } 50 | 51 | -------------------------------------------------------------------------------- /ReorderStackView/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | } 33 | ], 34 | "info" : { 35 | "version" : 1, 36 | "author" : "xcode" 37 | } 38 | } -------------------------------------------------------------------------------- /ReorderStackView/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /ReorderStackView/ExampleView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExampleView.swift 3 | // ReorderStackView 4 | // 5 | // Created by Clay Ellis on 10/16/15. 6 | // Copyright © 2015 Clay Ellis. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ExampleView: UIView { 12 | 13 | let rStackView = APRedorderableStackView() 14 | 15 | var rViews = [RView]() 16 | 17 | convenience init() { 18 | self.init(frame: CGRect.zero) 19 | 20 | for index in 1 ... 4 { 21 | var color: String! 22 | var height: CGFloat! 23 | switch index { 24 | case 1: color = "385C69"; height = 100 25 | case 2: color = "5993A9"; height = 130 26 | case 3: color = "619FB6"; height = 50 27 | default: color = "81D6F5"; height = 70 28 | } 29 | self.rViews.append(RView(num: index, color: color, height: height)) 30 | } 31 | } 32 | 33 | override init(frame: CGRect) { 34 | super.init(frame: frame) 35 | self.setNeedsUpdateConstraints() 36 | } 37 | 38 | required init?(coder aDecoder: NSCoder) { 39 | fatalError("init(coder:) has not been implemented") 40 | } 41 | 42 | func configureSubviews() { 43 | // Add Subviews 44 | self.addSubview(self.rStackView) 45 | for rView in self.rViews { 46 | self.rStackView.addArrangedSubview(rView) 47 | } 48 | 49 | // Style View 50 | self.backgroundColor = .white 51 | 52 | // Style Subviews 53 | self.rStackView.axis = .vertical 54 | self.rStackView.distribution = .fillProportionally 55 | self.rStackView.alignment = .fill 56 | self.rStackView.clipsToBounds = false 57 | 58 | // ---------------------------------------------------------------------------- 59 | // Set reorderingEnabled to true to, well, enable reordering 60 | self.rStackView.reorderingEnabled = true 61 | // ---------------------------------------------------------------------------- 62 | 63 | } 64 | 65 | override func updateConstraints() { 66 | // Configure Subviews 67 | self.configureSubviews() 68 | 69 | // Add Constraints 70 | self.rStackView.translatesAutoresizingMaskIntoConstraints = false 71 | let left = NSLayoutConstraint(item: self.rStackView, attribute: .left, relatedBy: .equal, toItem: self, attribute: .left, multiplier: 1.0, constant: 15) 72 | let right = NSLayoutConstraint(item: self.rStackView, attribute: .right, relatedBy: .equal, toItem: self, attribute: .right, multiplier: 1.0, constant: -15) 73 | let top = NSLayoutConstraint(item: self.rStackView, attribute: .top, relatedBy: .equal, toItem: self, attribute: .top, multiplier: 1.0, constant: 40) 74 | let bottom = NSLayoutConstraint(item: self.rStackView, attribute: .bottom, relatedBy: .equal, toItem: self, attribute: .bottom, multiplier: 1.0, constant: -40) 75 | 76 | self.addConstraint(left) 77 | self.addConstraint(right) 78 | self.addConstraint(top) 79 | self.addConstraint(bottom) 80 | 81 | super.updateConstraints() 82 | } 83 | } 84 | 85 | class RView: UIView { 86 | 87 | // Data 88 | var num = 0 89 | var color = "000000" 90 | var height: CGFloat = 150 91 | 92 | // Subviews 93 | let label = UILabel() 94 | 95 | convenience init(num: Int, color: String, height: CGFloat) { 96 | self.init(frame: CGRect.zero) 97 | self.num = num 98 | self.color = color 99 | self.height = height 100 | } 101 | 102 | override init(frame: CGRect) { 103 | super.init(frame: frame) 104 | self.setNeedsUpdateConstraints() 105 | } 106 | 107 | required init?(coder aDecoder: NSCoder) { 108 | fatalError("init(coder:) has not been implemented") 109 | } 110 | 111 | func configureSubviews() { 112 | // Add Subviews 113 | self.addSubview(self.label) 114 | 115 | // Style View 116 | self.backgroundColor = UIColor(hexString: self.color) 117 | 118 | // Style Subviews 119 | self.label.text = "\(self.num)" 120 | self.label.textColor = .black 121 | self.label.textAlignment = .center 122 | 123 | } 124 | 125 | override func updateConstraints() { 126 | // Configure Subviews 127 | self.configureSubviews() 128 | 129 | // Add Constraints 130 | self.label.translatesAutoresizingMaskIntoConstraints = false 131 | let left = NSLayoutConstraint(item: self.label, attribute: .left, relatedBy: .equal, toItem: self, attribute: .left, multiplier: 1.0, constant: 0) 132 | let right = NSLayoutConstraint(item: self.label, attribute: .right, relatedBy: .equal, toItem: self, attribute: .right, multiplier: 1.0, constant: 0) 133 | let top = NSLayoutConstraint(item: self.label, attribute: .top, relatedBy: .equal, toItem: self, attribute: .top, multiplier: 1.0, constant: 0) 134 | let bottom = NSLayoutConstraint(item: self.label, attribute: .bottom, relatedBy: .equal, toItem: self, attribute: .bottom, multiplier: 1.0, constant: 0) 135 | 136 | self.addConstraint(left) 137 | self.addConstraint(right) 138 | self.addConstraint(top) 139 | self.addConstraint(bottom) 140 | 141 | super.updateConstraints() 142 | } 143 | 144 | override var intrinsicContentSize : CGSize { 145 | return CGSize(width: 10, height: self.height) 146 | } 147 | 148 | } 149 | -------------------------------------------------------------------------------- /ReorderStackView/ExampleViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExampleViewController.swift 3 | // ReorderStackView 4 | // 5 | // Created by Clay Ellis on 10/16/15. 6 | // Copyright © 2015 Clay Ellis. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ExampleViewController: UIViewController, APStackViewReorderDelegate { 12 | 13 | let exampleView = ExampleView() 14 | 15 | override func loadView() { 16 | self.view = self.exampleView 17 | } 18 | 19 | override func viewDidLoad() { 20 | super.viewDidLoad() 21 | 22 | // Attach the delegate 23 | self.exampleView.rStackView.reorderDelegate = self 24 | } 25 | 26 | // Delegate Methods 27 | func didBeginReordering() { 28 | print("Did begin reordering") 29 | } 30 | 31 | func didDragToReorder(inUpDirection up: Bool, maxY: CGFloat, minY: CGFloat) { 32 | print("Dragging: \(up ? "Up" : "Down")") 33 | } 34 | 35 | 36 | func didEndReordering() { 37 | print("Did end reordering") 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /ReorderStackView/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /UIColorExtension.swift: -------------------------------------------------------------------------------- 1 | // SwiftHEXColors.swift 2 | // 3 | // Copyright (c) 2014 SwiftHEXColors contributors 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | // THE SOFTWARE. 22 | 23 | #if os(iOS) || os(tvOS) 24 | import UIKit 25 | typealias SWColor = UIColor 26 | #else 27 | import Cocoa 28 | typealias SWColor = NSColor 29 | #endif 30 | 31 | private extension Int { 32 | func duplicate4bits() -> Int { 33 | return (self << 4) + self 34 | } 35 | } 36 | 37 | /// An extension of UIColor (on iOS) or NSColor (on OSX) providing HEX color handling. 38 | public extension SWColor { 39 | /** 40 | Create non-autoreleased color with in the given hex string. Alpha will be set as 1 by default. 41 | 42 | - parameter hexString: The hex string, with or without the hash character. 43 | - returns: A color with the given hex string. 44 | */ 45 | convenience init?(hexString: String) { 46 | self.init(hexString: hexString, alpha: 1.0) 47 | } 48 | 49 | private convenience init?(hex3: Int, alpha: Float) { 50 | self.init(red: CGFloat( ((hex3 & 0xF00) >> 8).duplicate4bits() ) / 255.0, 51 | green: CGFloat( ((hex3 & 0x0F0) >> 4).duplicate4bits() ) / 255.0, 52 | blue: CGFloat( ((hex3 & 0x00F) >> 0).duplicate4bits() ) / 255.0, 53 | alpha: CGFloat(alpha)) 54 | } 55 | 56 | private convenience init?(hex6: Int, alpha: Float) { 57 | self.init(red: CGFloat( (hex6 & 0xFF0000) >> 16 ) / 255.0, 58 | green: CGFloat( (hex6 & 0x00FF00) >> 8 ) / 255.0, 59 | blue: CGFloat( (hex6 & 0x0000FF) >> 0 ) / 255.0, alpha: CGFloat(alpha)) 60 | } 61 | 62 | /** 63 | Create non-autoreleased color with in the given hex string and alpha. 64 | 65 | - parameter hexString: The hex string, with or without the hash character. 66 | - parameter alpha: The alpha value, a floating value between 0 and 1. 67 | - returns: A color with the given hex string and alpha. 68 | */ 69 | convenience init?(hexString: String, alpha: Float) { 70 | var hex = hexString 71 | 72 | // Check for hash and remove the hash 73 | if hex.hasPrefix("#") { 74 | hex = String(hex[hex.index(after: hex.startIndex)...]) 75 | } 76 | 77 | guard let hexVal = Int(hex, radix: 16) else { 78 | self.init() 79 | return nil 80 | } 81 | 82 | switch hex.count { 83 | case 3: 84 | self.init(hex3: hexVal, alpha: alpha) 85 | case 6: 86 | self.init(hex6: hexVal, alpha: alpha) 87 | default: 88 | // Note: 89 | // The swift 1.1 compiler is currently unable to destroy partially initialized classes in all cases, 90 | // so it disallows formation of a situation where it would have to. We consider this a bug to be fixed 91 | // in future releases, not a feature. -- Apple Forum 92 | self.init() 93 | return nil 94 | } 95 | } 96 | 97 | /** 98 | Create non-autoreleased color with in the given hex value. Alpha will be set as 1 by default. 99 | 100 | - parameter hex: The hex value. For example: 0xff8942 (no quotation). 101 | - returns: A color with the given hex value 102 | */ 103 | convenience init?(hex: Int) { 104 | self.init(hex: hex, alpha: 1.0) 105 | } 106 | 107 | /** 108 | Create non-autoreleased color with in the given hex value and alpha 109 | 110 | - parameter hex: The hex value. For example: 0xff8942 (no quotation). 111 | - parameter alpha: The alpha value, a floating value between 0 and 1. 112 | - returns: color with the given hex value and alpha 113 | */ 114 | convenience init?(hex: Int, alpha: Float) { 115 | if (0x000000 ... 0xFFFFFF) ~= hex { 116 | self.init(hex6: hex, alpha: alpha) 117 | } else { 118 | self.init() 119 | return nil 120 | } 121 | } 122 | } 123 | --------------------------------------------------------------------------------