├── .gitignore ├── Example └── CropperExample │ ├── CropperExample.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ └── CropperExample │ ├── AppDelegate.swift │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── Contents.json │ └── image.imageset │ │ ├── 12249714_10153376954632950_4883332664187659092_n.jpg │ │ └── Contents.json │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── Info.plist │ └── ViewController.swift ├── LICENSE ├── README.md ├── UIImageCropper.podspec └── UIImageCropper ├── UIImage+FixOrientation.swift └── UIImageCropper.swift /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/macos,swift,xcode,cocoapods,objective-c 3 | 4 | ### CocoaPods ### 5 | ## CocoaPods GitIgnore Template 6 | 7 | # CocoaPods - Only use to conserve bandwidth / Save time on Pushing 8 | # - Also handy if you have a lage number of dependant pods 9 | # - AS PER https://guides.cocoapods.org/using/using-cocoapods.html NEVER IGONRE THE LOCK FILE 10 | Pods/ 11 | 12 | ### macOS ### 13 | *.DS_Store 14 | .AppleDouble 15 | .LSOverride 16 | 17 | # Icon must end with two \r 18 | Icon 19 | 20 | # Thumbnails 21 | ._* 22 | 23 | # Files that might appear in the root of a volume 24 | .DocumentRevisions-V100 25 | .fseventsd 26 | .Spotlight-V100 27 | .TemporaryItems 28 | .Trashes 29 | .VolumeIcon.icns 30 | .com.apple.timemachine.donotpresent 31 | 32 | # Directories potentially created on remote AFP share 33 | .AppleDB 34 | .AppleDesktop 35 | Network Trash Folder 36 | Temporary Items 37 | .apdisk 38 | 39 | ### Objective-C ### 40 | # Xcode 41 | # 42 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 43 | 44 | ## Build generated 45 | build/ 46 | DerivedData/ 47 | 48 | ## Various settings 49 | *.pbxuser 50 | !default.pbxuser 51 | *.mode1v3 52 | !default.mode1v3 53 | *.mode2v3 54 | !default.mode2v3 55 | *.perspectivev3 56 | !default.perspectivev3 57 | xcuserdata/ 58 | 59 | ## Other 60 | *.moved-aside 61 | *.xccheckout 62 | *.xcscmblueprint 63 | 64 | ## Obj-C/Swift specific 65 | *.hmap 66 | *.ipa 67 | *.dSYM.zip 68 | *.dSYM 69 | 70 | # CocoaPods - Refactored to standalone file 71 | 72 | 73 | # Carthage - Refactored to standalone file 74 | 75 | # fastlane 76 | # 77 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 78 | # screenshots whenever they are needed. 79 | # For more information about the recommended setup visit: 80 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 81 | 82 | fastlane/report.xml 83 | fastlane/Preview.html 84 | fastlane/screenshots 85 | fastlane/test_output 86 | 87 | # Code Injection 88 | # 89 | # After new code Injection tools there's a generated folder /iOSInjectionProject 90 | # https://github.com/johnno1962/injectionforxcode 91 | 92 | iOSInjectionProject/ 93 | 94 | ### Objective-C Patch ### 95 | 96 | ### Swift ### 97 | # Xcode 98 | # 99 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 100 | 101 | ## Build generated 102 | 103 | ## Various settings 104 | 105 | ## Other 106 | 107 | ## Obj-C/Swift specific 108 | 109 | ## Playgrounds 110 | timeline.xctimeline 111 | playground.xcworkspace 112 | 113 | # Swift Package Manager 114 | # 115 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 116 | # Packages/ 117 | # Package.pins 118 | .build/ 119 | 120 | # CocoaPods - Refactored to standalone file 121 | 122 | # Carthage - Refactored to standalone file 123 | 124 | # fastlane 125 | # 126 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 127 | # screenshots whenever they are needed. 128 | # For more information about the recommended setup visit: 129 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 130 | 131 | 132 | ### Xcode ### 133 | # Xcode 134 | # 135 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 136 | 137 | ## Build generated 138 | 139 | ## Various settings 140 | 141 | ## Other 142 | 143 | ### Xcode Patch ### 144 | *.xcodeproj/* 145 | !*.xcodeproj/project.pbxproj 146 | !*.xcodeproj/xcshareddata/ 147 | !*.xcworkspace/contents.xcworkspacedata 148 | /*.gcno 149 | 150 | # End of https://www.gitignore.io/api/macos,swift,xcode,cocoapods,objective-c -------------------------------------------------------------------------------- /Example/CropperExample/CropperExample.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 48; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 49329A1A1FFB79F600741BBA /* UIImage+FixOrientation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49329A191FFB79F600741BBA /* UIImage+FixOrientation.swift */; }; 11 | 49B9550A1FF5795B00BA9181 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49B955091FF5795B00BA9181 /* AppDelegate.swift */; }; 12 | 49B9550C1FF5795B00BA9181 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49B9550B1FF5795B00BA9181 /* ViewController.swift */; }; 13 | 49B9550F1FF5795B00BA9181 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 49B9550D1FF5795B00BA9181 /* Main.storyboard */; }; 14 | 49B955111FF5795B00BA9181 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 49B955101FF5795B00BA9181 /* Assets.xcassets */; }; 15 | 49B955141FF5795B00BA9181 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 49B955121FF5795B00BA9181 /* LaunchScreen.storyboard */; }; 16 | 49B9551E1FF57EC000BA9181 /* UIImageCropper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49B9551D1FF57EBF00BA9181 /* UIImageCropper.swift */; }; 17 | /* End PBXBuildFile section */ 18 | 19 | /* Begin PBXFileReference section */ 20 | 491DAAE820CA66A500A1C33A /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../../README.md; sourceTree = ""; }; 21 | 49329A191FFB79F600741BBA /* UIImage+FixOrientation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "UIImage+FixOrientation.swift"; path = "../../UIImageCropper/UIImage+FixOrientation.swift"; sourceTree = ""; }; 22 | 49B955061FF5795B00BA9181 /* CropperExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CropperExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 23 | 49B955091FF5795B00BA9181 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 24 | 49B9550B1FF5795B00BA9181 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 25 | 49B9550E1FF5795B00BA9181 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 26 | 49B955101FF5795B00BA9181 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 27 | 49B955131FF5795B00BA9181 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 28 | 49B955151FF5795B00BA9181 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 29 | 49B9551D1FF57EBF00BA9181 /* UIImageCropper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = UIImageCropper.swift; path = ../../UIImageCropper/UIImageCropper.swift; sourceTree = ""; }; 30 | /* End PBXFileReference section */ 31 | 32 | /* Begin PBXFrameworksBuildPhase section */ 33 | 49B955031FF5795B00BA9181 /* Frameworks */ = { 34 | isa = PBXFrameworksBuildPhase; 35 | buildActionMask = 2147483647; 36 | files = ( 37 | ); 38 | runOnlyForDeploymentPostprocessing = 0; 39 | }; 40 | /* End PBXFrameworksBuildPhase section */ 41 | 42 | /* Begin PBXGroup section */ 43 | 49B954FD1FF5795B00BA9181 = { 44 | isa = PBXGroup; 45 | children = ( 46 | 49B955081FF5795B00BA9181 /* CropperExample */, 47 | 49B955071FF5795B00BA9181 /* Products */, 48 | 49B9551D1FF57EBF00BA9181 /* UIImageCropper.swift */, 49 | 49329A191FFB79F600741BBA /* UIImage+FixOrientation.swift */, 50 | 491DAAE820CA66A500A1C33A /* README.md */, 51 | ); 52 | sourceTree = ""; 53 | }; 54 | 49B955071FF5795B00BA9181 /* Products */ = { 55 | isa = PBXGroup; 56 | children = ( 57 | 49B955061FF5795B00BA9181 /* CropperExample.app */, 58 | ); 59 | name = Products; 60 | sourceTree = ""; 61 | }; 62 | 49B955081FF5795B00BA9181 /* CropperExample */ = { 63 | isa = PBXGroup; 64 | children = ( 65 | 49B955091FF5795B00BA9181 /* AppDelegate.swift */, 66 | 49B9550B1FF5795B00BA9181 /* ViewController.swift */, 67 | 49B9550D1FF5795B00BA9181 /* Main.storyboard */, 68 | 49B955101FF5795B00BA9181 /* Assets.xcassets */, 69 | 49B955121FF5795B00BA9181 /* LaunchScreen.storyboard */, 70 | 49B955151FF5795B00BA9181 /* Info.plist */, 71 | ); 72 | path = CropperExample; 73 | sourceTree = ""; 74 | }; 75 | /* End PBXGroup section */ 76 | 77 | /* Begin PBXNativeTarget section */ 78 | 49B955051FF5795B00BA9181 /* CropperExample */ = { 79 | isa = PBXNativeTarget; 80 | buildConfigurationList = 49B955181FF5795B00BA9181 /* Build configuration list for PBXNativeTarget "CropperExample" */; 81 | buildPhases = ( 82 | 49B955021FF5795B00BA9181 /* Sources */, 83 | 49B955031FF5795B00BA9181 /* Frameworks */, 84 | 49B955041FF5795B00BA9181 /* Resources */, 85 | ); 86 | buildRules = ( 87 | ); 88 | dependencies = ( 89 | ); 90 | name = CropperExample; 91 | productName = CropperExample; 92 | productReference = 49B955061FF5795B00BA9181 /* CropperExample.app */; 93 | productType = "com.apple.product-type.application"; 94 | }; 95 | /* End PBXNativeTarget section */ 96 | 97 | /* Begin PBXProject section */ 98 | 49B954FE1FF5795B00BA9181 /* Project object */ = { 99 | isa = PBXProject; 100 | attributes = { 101 | LastSwiftUpdateCheck = 0920; 102 | LastUpgradeCheck = 1000; 103 | ORGANIZATIONNAME = "Jari Kalinainen"; 104 | TargetAttributes = { 105 | 49B955051FF5795B00BA9181 = { 106 | CreatedOnToolsVersion = 9.2; 107 | LastSwiftMigration = 1000; 108 | ProvisioningStyle = Automatic; 109 | }; 110 | }; 111 | }; 112 | buildConfigurationList = 49B955011FF5795B00BA9181 /* Build configuration list for PBXProject "CropperExample" */; 113 | compatibilityVersion = "Xcode 8.0"; 114 | developmentRegion = en; 115 | hasScannedForEncodings = 0; 116 | knownRegions = ( 117 | en, 118 | Base, 119 | ); 120 | mainGroup = 49B954FD1FF5795B00BA9181; 121 | productRefGroup = 49B955071FF5795B00BA9181 /* Products */; 122 | projectDirPath = ""; 123 | projectRoot = ""; 124 | targets = ( 125 | 49B955051FF5795B00BA9181 /* CropperExample */, 126 | ); 127 | }; 128 | /* End PBXProject section */ 129 | 130 | /* Begin PBXResourcesBuildPhase section */ 131 | 49B955041FF5795B00BA9181 /* Resources */ = { 132 | isa = PBXResourcesBuildPhase; 133 | buildActionMask = 2147483647; 134 | files = ( 135 | 49B955141FF5795B00BA9181 /* LaunchScreen.storyboard in Resources */, 136 | 49B955111FF5795B00BA9181 /* Assets.xcassets in Resources */, 137 | 49B9550F1FF5795B00BA9181 /* Main.storyboard in Resources */, 138 | ); 139 | runOnlyForDeploymentPostprocessing = 0; 140 | }; 141 | /* End PBXResourcesBuildPhase section */ 142 | 143 | /* Begin PBXSourcesBuildPhase section */ 144 | 49B955021FF5795B00BA9181 /* Sources */ = { 145 | isa = PBXSourcesBuildPhase; 146 | buildActionMask = 2147483647; 147 | files = ( 148 | 49B9550C1FF5795B00BA9181 /* ViewController.swift in Sources */, 149 | 49B9551E1FF57EC000BA9181 /* UIImageCropper.swift in Sources */, 150 | 49329A1A1FFB79F600741BBA /* UIImage+FixOrientation.swift in Sources */, 151 | 49B9550A1FF5795B00BA9181 /* AppDelegate.swift in Sources */, 152 | ); 153 | runOnlyForDeploymentPostprocessing = 0; 154 | }; 155 | /* End PBXSourcesBuildPhase section */ 156 | 157 | /* Begin PBXVariantGroup section */ 158 | 49B9550D1FF5795B00BA9181 /* Main.storyboard */ = { 159 | isa = PBXVariantGroup; 160 | children = ( 161 | 49B9550E1FF5795B00BA9181 /* Base */, 162 | ); 163 | name = Main.storyboard; 164 | sourceTree = ""; 165 | }; 166 | 49B955121FF5795B00BA9181 /* LaunchScreen.storyboard */ = { 167 | isa = PBXVariantGroup; 168 | children = ( 169 | 49B955131FF5795B00BA9181 /* Base */, 170 | ); 171 | name = LaunchScreen.storyboard; 172 | sourceTree = ""; 173 | }; 174 | /* End PBXVariantGroup section */ 175 | 176 | /* Begin XCBuildConfiguration section */ 177 | 49B955161FF5795B00BA9181 /* Debug */ = { 178 | isa = XCBuildConfiguration; 179 | buildSettings = { 180 | ALWAYS_SEARCH_USER_PATHS = NO; 181 | CLANG_ANALYZER_NONNULL = YES; 182 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 183 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 184 | CLANG_CXX_LIBRARY = "libc++"; 185 | CLANG_ENABLE_MODULES = YES; 186 | CLANG_ENABLE_OBJC_ARC = YES; 187 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 188 | CLANG_WARN_BOOL_CONVERSION = YES; 189 | CLANG_WARN_COMMA = YES; 190 | CLANG_WARN_CONSTANT_CONVERSION = YES; 191 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 192 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 193 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 194 | CLANG_WARN_EMPTY_BODY = YES; 195 | CLANG_WARN_ENUM_CONVERSION = YES; 196 | CLANG_WARN_INFINITE_RECURSION = YES; 197 | CLANG_WARN_INT_CONVERSION = YES; 198 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 199 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 200 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 201 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 202 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 203 | CLANG_WARN_STRICT_PROTOTYPES = YES; 204 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 205 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 206 | CLANG_WARN_UNREACHABLE_CODE = YES; 207 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 208 | CODE_SIGN_IDENTITY = "iPhone Developer"; 209 | COPY_PHASE_STRIP = NO; 210 | DEBUG_INFORMATION_FORMAT = dwarf; 211 | ENABLE_STRICT_OBJC_MSGSEND = YES; 212 | ENABLE_TESTABILITY = YES; 213 | GCC_C_LANGUAGE_STANDARD = gnu11; 214 | GCC_DYNAMIC_NO_PIC = NO; 215 | GCC_NO_COMMON_BLOCKS = YES; 216 | GCC_OPTIMIZATION_LEVEL = 0; 217 | GCC_PREPROCESSOR_DEFINITIONS = ( 218 | "DEBUG=1", 219 | "$(inherited)", 220 | ); 221 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 222 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 223 | GCC_WARN_UNDECLARED_SELECTOR = YES; 224 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 225 | GCC_WARN_UNUSED_FUNCTION = YES; 226 | GCC_WARN_UNUSED_VARIABLE = YES; 227 | IPHONEOS_DEPLOYMENT_TARGET = 11.2; 228 | MTL_ENABLE_DEBUG_INFO = YES; 229 | ONLY_ACTIVE_ARCH = YES; 230 | SDKROOT = iphoneos; 231 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 232 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 233 | }; 234 | name = Debug; 235 | }; 236 | 49B955171FF5795B00BA9181 /* Release */ = { 237 | isa = XCBuildConfiguration; 238 | buildSettings = { 239 | ALWAYS_SEARCH_USER_PATHS = NO; 240 | CLANG_ANALYZER_NONNULL = YES; 241 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 242 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 243 | CLANG_CXX_LIBRARY = "libc++"; 244 | CLANG_ENABLE_MODULES = YES; 245 | CLANG_ENABLE_OBJC_ARC = YES; 246 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 247 | CLANG_WARN_BOOL_CONVERSION = YES; 248 | CLANG_WARN_COMMA = YES; 249 | CLANG_WARN_CONSTANT_CONVERSION = YES; 250 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 251 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 252 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 253 | CLANG_WARN_EMPTY_BODY = YES; 254 | CLANG_WARN_ENUM_CONVERSION = YES; 255 | CLANG_WARN_INFINITE_RECURSION = YES; 256 | CLANG_WARN_INT_CONVERSION = YES; 257 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 258 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 259 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 260 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 261 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 262 | CLANG_WARN_STRICT_PROTOTYPES = YES; 263 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 264 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 265 | CLANG_WARN_UNREACHABLE_CODE = YES; 266 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 267 | CODE_SIGN_IDENTITY = "iPhone Developer"; 268 | COPY_PHASE_STRIP = NO; 269 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 270 | ENABLE_NS_ASSERTIONS = NO; 271 | ENABLE_STRICT_OBJC_MSGSEND = YES; 272 | GCC_C_LANGUAGE_STANDARD = gnu11; 273 | GCC_NO_COMMON_BLOCKS = YES; 274 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 275 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 276 | GCC_WARN_UNDECLARED_SELECTOR = YES; 277 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 278 | GCC_WARN_UNUSED_FUNCTION = YES; 279 | GCC_WARN_UNUSED_VARIABLE = YES; 280 | IPHONEOS_DEPLOYMENT_TARGET = 11.2; 281 | MTL_ENABLE_DEBUG_INFO = NO; 282 | SDKROOT = iphoneos; 283 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 284 | VALIDATE_PRODUCT = YES; 285 | }; 286 | name = Release; 287 | }; 288 | 49B955191FF5795B00BA9181 /* Debug */ = { 289 | isa = XCBuildConfiguration; 290 | buildSettings = { 291 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 292 | CODE_SIGN_STYLE = Automatic; 293 | DEVELOPMENT_TEAM = 23Z355DMXW; 294 | INFOPLIST_FILE = CropperExample/Info.plist; 295 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 296 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 297 | PRODUCT_BUNDLE_IDENTIFIER = com.klubitii.CropperExample; 298 | PRODUCT_NAME = "$(TARGET_NAME)"; 299 | SWIFT_VERSION = 4.2; 300 | TARGETED_DEVICE_FAMILY = "1,2"; 301 | }; 302 | name = Debug; 303 | }; 304 | 49B9551A1FF5795B00BA9181 /* Release */ = { 305 | isa = XCBuildConfiguration; 306 | buildSettings = { 307 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 308 | CODE_SIGN_STYLE = Automatic; 309 | DEVELOPMENT_TEAM = 23Z355DMXW; 310 | INFOPLIST_FILE = CropperExample/Info.plist; 311 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 312 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 313 | PRODUCT_BUNDLE_IDENTIFIER = com.klubitii.CropperExample; 314 | PRODUCT_NAME = "$(TARGET_NAME)"; 315 | SWIFT_VERSION = 4.2; 316 | TARGETED_DEVICE_FAMILY = "1,2"; 317 | }; 318 | name = Release; 319 | }; 320 | /* End XCBuildConfiguration section */ 321 | 322 | /* Begin XCConfigurationList section */ 323 | 49B955011FF5795B00BA9181 /* Build configuration list for PBXProject "CropperExample" */ = { 324 | isa = XCConfigurationList; 325 | buildConfigurations = ( 326 | 49B955161FF5795B00BA9181 /* Debug */, 327 | 49B955171FF5795B00BA9181 /* Release */, 328 | ); 329 | defaultConfigurationIsVisible = 0; 330 | defaultConfigurationName = Release; 331 | }; 332 | 49B955181FF5795B00BA9181 /* Build configuration list for PBXNativeTarget "CropperExample" */ = { 333 | isa = XCConfigurationList; 334 | buildConfigurations = ( 335 | 49B955191FF5795B00BA9181 /* Debug */, 336 | 49B9551A1FF5795B00BA9181 /* Release */, 337 | ); 338 | defaultConfigurationIsVisible = 0; 339 | defaultConfigurationName = Release; 340 | }; 341 | /* End XCConfigurationList section */ 342 | }; 343 | rootObject = 49B954FE1FF5795B00BA9181 /* Project object */; 344 | } 345 | -------------------------------------------------------------------------------- /Example/CropperExample/CropperExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/CropperExample/CropperExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/CropperExample/CropperExample/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // CropperExample 4 | // 5 | // Created by Jari Kalinainen on 28.12.17. 6 | // Copyright © 2017 Jari Kalinainen. 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 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(_ application: UIApplication) { 23 | // 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. 24 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(_ application: UIApplication) { 28 | // 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. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(_ application: UIApplication) { 33 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | func applicationDidBecomeActive(_ application: UIApplication) { 37 | // 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. 38 | } 39 | 40 | func applicationWillTerminate(_ application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /Example/CropperExample/CropperExample/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /Example/CropperExample/CropperExample/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Example/CropperExample/CropperExample/Assets.xcassets/image.imageset/12249714_10153376954632950_4883332664187659092_n.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jvk75/UIImageCropper/fb888c0c0eb942c466f8224f328bcabbc3411da9/Example/CropperExample/CropperExample/Assets.xcassets/image.imageset/12249714_10153376954632950_4883332664187659092_n.jpg -------------------------------------------------------------------------------- /Example/CropperExample/CropperExample/Assets.xcassets/image.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "12249714_10153376954632950_4883332664187659092_n.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/CropperExample/CropperExample/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 | -------------------------------------------------------------------------------- /Example/CropperExample/CropperExample/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 82 | 89 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | -------------------------------------------------------------------------------- /Example/CropperExample/CropperExample/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 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 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 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 | NSCameraUsageDescription 45 | To take pictures 46 | 47 | 48 | -------------------------------------------------------------------------------- /Example/CropperExample/CropperExample/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // CropperExample 4 | // 5 | // Created by Jari Kalinainen on 28.12.17. 6 | // Copyright © 2017 Jari Kalinainen. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ViewController: UIViewController { 12 | 13 | @IBOutlet weak var imageView: UIImageView! 14 | 15 | private let picker = UIImagePickerController() 16 | private let cropper = UIImageCropper(cropRatio: 2/3) 17 | 18 | override func viewDidLoad() { 19 | super.viewDidLoad() 20 | 21 | //setup the cropper 22 | cropper.delegate = self 23 | //cropper.cropRatio = 2/3 //(can be set during runtime or in init) 24 | cropper.cropButtonText = "Crop" // this can be localized if needed (as well as the cancelButtonText) 25 | } 26 | 27 | @IBAction func closeView(_ sender: Any) { 28 | self.dismiss(animated: true, completion: nil) 29 | } 30 | 31 | @IBAction func cropExistingImage(_ sender: Any) { 32 | let cropper = UIImageCropper(cropRatio: 2/3) 33 | cropper.delegate = self 34 | cropper.picker = nil 35 | cropper.image = UIImage(named: "image") 36 | cropper.cancelButtonText = "Cancel" 37 | self.present(cropper, animated: true, completion: nil) 38 | } 39 | 40 | @IBAction func takePicturePressed(_ sender: Any) { 41 | cropper.picker = picker 42 | let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) 43 | alertController.popoverPresentationController?.sourceView = self.view 44 | alertController.popoverPresentationController?.sourceRect = CGRect(origin: self.view.center, size: CGSize.zero) 45 | 46 | cropper.cancelButtonText = "Retake" 47 | 48 | if UIImagePickerController.isSourceTypeAvailable(.camera) { 49 | alertController.addAction(UIAlertAction(title: NSLocalizedString("Camera", comment: ""), style: .default) { _ in 50 | self.picker.sourceType = .camera 51 | self.present(self.picker, animated: true, completion: nil) 52 | }) 53 | } 54 | alertController.addAction(UIAlertAction(title: NSLocalizedString("Gallery", comment: ""), style: .default) { _ in 55 | self.picker.sourceType = .photoLibrary 56 | self.present(self.picker, animated: true, completion: nil) 57 | }) 58 | alertController.addAction(UIAlertAction(title: NSLocalizedString("Cancel", comment: ""), style: .cancel, handler: { _ in 59 | })) 60 | self.present(alertController, animated: true, completion: nil) 61 | } 62 | } 63 | 64 | extension ViewController: UIImageCropperProtocol { 65 | func didCropImage(originalImage: UIImage?, croppedImage: UIImage?) { 66 | imageView.image = croppedImage 67 | } 68 | 69 | //optional 70 | func didCancel() { 71 | picker.dismiss(animated: true, completion: nil) 72 | print("did cancel") 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Jari Kalinainen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UIImageCropper 2 | 3 | Simple Image cropper for UIImagePickerController with customisable crop aspect ratio. Made purely with Swift 4! 4 | 5 | Replaces the iOS "crop only to square" functionality. Easy few line setup with delegate method/s. 6 | 7 | ## Requirements 8 | 9 | - iOS10+ 10 | - Xcode 9.2+ 11 | - Swift 4 12 | 13 | 14 | ## Install 15 | UIImageCropper is available through [CocoaPods](http://cocoapods.org). To install 16 | it, simply add the following line to your Podfile: 17 | 18 | ``` ruby 19 | pod 'UIImageCropper' 20 | ``` 21 | 22 | (or add UIImageCropper folder to your project) 23 | 24 | ## Usage 25 | 26 | Import the pod 27 | 28 | ``` swift 29 | import UIImageCropper 30 | ``` 31 | 32 | Create instanses of UIImageCropper and UIImagePickerController *(optional, if cropping existing UIImage)* 33 | 34 | UIImageCropper can take `cropRatio` as parameter. Default ratio is 1 (square). 35 | 36 | ``` swift 37 | let picker = UIImagePickerController() 38 | let cropper = UIImageCropper(cropRatio: 2/3) 39 | ``` 40 | 41 | Setup UIImageCropper 42 | 43 | ``` swift 44 | cropper.picker = picker 45 | cropper.delegate = self 46 | //cropper.cropRatio = 2/3 //(can be set with variable, before cropper is presented, or in cropper init) 47 | //cropper.cropButtonText = "Crop" // button labes can be localised/changed 48 | //cropper.cancelButtonText = "Cancel" 49 | 50 | ``` 51 | 52 | If just cropping existing UIImage there is no need to set up picker, delegate is enough. 53 | Just give image to croppen and present it. 54 | 55 | ``` swift 56 | cropper.picker = nil // Make sure not set the picker when doing existing image cropping 57 | cropper.image = UIImage(named: "image") 58 | viewController.present(cropper, animated: true, completion: nil) 59 | ``` 60 | 61 | For both cases implement `UIImageCropperProtocol` delegate method/s 62 | 63 | ``` swift 64 | func didCropImage(originalImage: UIImage?, croppedImage: UIImage?) { 65 | imageView.image = croppedImage 66 | } 67 | 68 | //optional (if not implemented cropper will close itself and picker) 69 | func didCancel() { 70 | picker.dismiss(animated: true, completion: nil) 71 | } 72 | 73 | ``` 74 | 75 | The UIImageCropper will handle the image picking (delegate methods). To start image picking just present the UIImagePickerController instance. 76 | 77 | ``` swift 78 | self.present(self.picker, animated: true, completion: nil) 79 | ``` 80 | 81 | For full usage exmaple see **CropperExample** in Example folder. 82 | 83 | ## Issues and contribution 84 | 85 | If you find any issues please add and issue to this repository. 86 | 87 | Improvements and/or fixes as pull requests are more than welcome. 88 | 89 | ## Author 90 | 91 | Jari Kalinainen, jari(a)klubitii.com 92 | 93 | ## License 94 | 95 | UIImageCropper is available under the MIT license. See the LICENSE file for more info. 96 | -------------------------------------------------------------------------------- /UIImageCropper.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # Be sure to run `pod lib lint NFCNDEFParse.podspec' to ensure this is a 3 | # valid spec before submitting. 4 | # 5 | # Any lines starting with a # are optional, but their use is encouraged 6 | # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html 7 | # 8 | 9 | Pod::Spec.new do |s| 10 | s.name = 'UIImageCropper' 11 | s.version = '1.4.0' 12 | s.summary = 'Simple Image cropper for UIImage and UIImagePickerController with customisable aspect ratio.' 13 | 14 | # This description is used to generate tags and improve search results. 15 | # * Think: What does it do? Why did you write it? What is the focus? 16 | # * Try to keep it short, snappy and to the point. 17 | # * Write the description between the DESC delimiters below. 18 | # * Finally, don't worry about the indent, CocoaPods strips it! 19 | 20 | s.description = <<-DESC 21 | Simple Image cropper for UIImage and UIImagePickerController with customisable crop aspect ratio. Made purely with Swift! 22 | Replaces the iOS "crop only to square" functionality. Easy few line setup with delegate method. With possibility to localized button texts. 23 | See example for usage details. 24 | DESC 25 | 26 | s.homepage = 'https://github.com/jvk75/UIImageCropper' 27 | s.license = { :type => 'MIT', :file => 'LICENSE' } 28 | s.author = { 'Jari Kalinainen' => 'jari@klubitii.com' } 29 | s.source = { :git => 'https://github.com/jvk75/UIImageCropper.git', :tag => s.version.to_s } 30 | s.swift_version = '4.2' 31 | 32 | s.ios.deployment_target = '10.0' 33 | 34 | s.source_files = 'UIImageCropper/*' 35 | 36 | end 37 | -------------------------------------------------------------------------------- /UIImageCropper/UIImage+FixOrientation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // 3 | 4 | import UIKit 5 | 6 | extension UIImage { 7 | 8 | func fixOrientation() -> UIImage { 9 | 10 | if imageOrientation == .up { 11 | return self 12 | } 13 | 14 | var transform: CGAffineTransform = CGAffineTransform.identity 15 | 16 | switch imageOrientation { 17 | case .down, .downMirrored: 18 | transform = transform.translatedBy(x: size.width, y: size.height) 19 | transform = transform.rotated(by: .pi) 20 | case .left, .leftMirrored: 21 | transform = transform.translatedBy(x: size.width, y: 0) 22 | transform = transform.rotated(by: .pi/2) 23 | case .right, .rightMirrored: 24 | transform = transform.translatedBy(x: 0, y: size.height) 25 | transform = transform.rotated(by: -.pi/2) 26 | default: //.up, .upMirrored 27 | break 28 | } 29 | 30 | switch imageOrientation { 31 | case .upMirrored, .downMirrored: 32 | transform.translatedBy(x: size.width, y: 0) 33 | transform.scaledBy(x: -1, y: 1) 34 | case .leftMirrored, .rightMirrored: 35 | transform.translatedBy(x: size.height, y: 0) 36 | transform.scaledBy(x: -1, y: 1) 37 | default: //.up, .down, .left, .right 38 | break 39 | } 40 | 41 | let ctx: CGContext = CGContext(data: nil, width: Int(size.width), height: Int(size.height), bitsPerComponent: self.cgImage!.bitsPerComponent, bytesPerRow: 0, space: (self.cgImage?.colorSpace)!, bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)! 42 | 43 | ctx.concatenate(transform) 44 | 45 | switch imageOrientation { 46 | case .left, .leftMirrored, .right, .rightMirrored: 47 | ctx.draw(self.cgImage!, in: CGRect(x: 0, y: 0, width: size.height, height: size.width)) 48 | default: 49 | ctx.draw(self.cgImage!, in: CGRect(x: 0, y: 0, width: size.width, height: size.height)) 50 | } 51 | 52 | let cgImage: CGImage = ctx.makeImage()! 53 | 54 | return UIImage(cgImage: cgImage) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /UIImageCropper/UIImageCropper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImageCropper.swift 3 | // UIImageCropper 4 | // 5 | // Created by Jari Kalinainen jari@klubitii.com 6 | // 7 | // Licensed under MIT License 2017 8 | // 9 | 10 | import UIKit 11 | 12 | @objc public protocol UIImageCropperProtocol: class { 13 | /// Called when user presses crop button (or when there is unknown situation (one or both images will be nil)). 14 | /// - parameter originalImage 15 | /// Orginal image from camera/gallery 16 | /// - parameter croppedImage 17 | /// Cropped image in cropRatio aspect ratio 18 | func didCropImage(originalImage: UIImage?, croppedImage: UIImage?) 19 | /// (optional) Called when user cancels the picker. If method is not available picker is dismissed. 20 | @objc optional func didCancel() 21 | } 22 | 23 | public class UIImageCropper: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate { 24 | 25 | /// Aspect ratio of the cropped image 26 | public var cropRatio: CGFloat = 1 27 | /// delegate that implements UIImageCropperProtocol 28 | public weak var delegate: UIImageCropperProtocol? 29 | /// UIImagePickerController picker 30 | public weak var picker: UIImagePickerController? { 31 | didSet { 32 | picker?.delegate = self 33 | picker?.allowsEditing = false 34 | } 35 | } 36 | 37 | /// Crop button text 38 | public var cropButtonText: String = "Crop" 39 | /// Retake/Cancel button text 40 | public var cancelButtonText: String = "Retake" 41 | 42 | /// original image from camera or gallery 43 | public var image: UIImage? { 44 | didSet { 45 | guard let image = self.image else { 46 | return 47 | } 48 | layoutDone = false 49 | ratio = image.size.height / image.size.width 50 | imageView.image = image 51 | self.view.layoutIfNeeded() 52 | } 53 | } 54 | /// cropped image 55 | public var cropImage: UIImage? { 56 | return crop() 57 | } 58 | 59 | /// autoClosePicker: if true, picker is dismissed when when image is cropped. When false parent needs to close picker. 60 | public var autoClosePicker: Bool = true 61 | 62 | private let topView = UIView() 63 | private let fadeView = UIView() 64 | private let imageView: UIImageView = UIImageView() 65 | private let cropView: UIView = UIView() 66 | 67 | private var topConst: NSLayoutConstraint? 68 | private var leadConst: NSLayoutConstraint? 69 | 70 | private var imageHeightConst: NSLayoutConstraint? 71 | private var imageWidthConst: NSLayoutConstraint? 72 | 73 | private var ratio: CGFloat = 1 74 | private var layoutDone: Bool = false 75 | 76 | private var orgHeight: CGFloat = 0 77 | private var orgWidth: CGFloat = 0 78 | private var topStart: CGFloat = 0 79 | private var leadStart: CGFloat = 0 80 | private var pinchStart: CGPoint = .zero 81 | 82 | private let cropButton = UIButton(type: .custom) 83 | private let cancelButton = UIButton(type: .custom) 84 | 85 | //MARK: - inits 86 | /// initializer 87 | /// - parameter cropRatio 88 | /// Aspect ratio of the cropped image 89 | convenience public init(cropRatio: CGFloat) { 90 | self.init() 91 | self.cropRatio = cropRatio 92 | } 93 | 94 | //MARK: - overrides 95 | override public func viewDidLoad() { 96 | super.viewDidLoad() 97 | 98 | self.view.backgroundColor = UIColor.black 99 | 100 | //main views 101 | topView.backgroundColor = UIColor.clear 102 | let bottomView = UIView() 103 | bottomView.backgroundColor = UIColor.black.withAlphaComponent(0.7) 104 | self.view.addSubview(topView) 105 | self.view.addSubview(bottomView) 106 | topView.translatesAutoresizingMaskIntoConstraints = false 107 | bottomView.translatesAutoresizingMaskIntoConstraints = false 108 | let horizontalTopConst = NSLayoutConstraint.constraints(withVisualFormat: "H:|-(0)-[view]-(0)-|", options: NSLayoutConstraint.FormatOptions(), metrics: nil, views: ["view": topView]) 109 | let horizontalBottomConst = NSLayoutConstraint.constraints(withVisualFormat: "H:|-(0)-[view]-(0)-|", options: NSLayoutConstraint.FormatOptions(), metrics: nil, views: ["view": bottomView]) 110 | let verticalConst = NSLayoutConstraint.constraints(withVisualFormat: "V:|-(0)-[top]-(0)-[bottom(70)]-|", options: NSLayoutConstraint.FormatOptions(), metrics: nil, views: ["bottom": bottomView, "top": topView]) 111 | self.view.addConstraints(horizontalTopConst + horizontalBottomConst + verticalConst) 112 | 113 | // image view 114 | imageView.contentMode = .scaleAspectFit 115 | imageView.translatesAutoresizingMaskIntoConstraints = false 116 | topView.addSubview(imageView) 117 | topConst = NSLayoutConstraint(item: imageView, attribute: .top, relatedBy: .equal, toItem: topView, attribute: .top, multiplier: 1, constant: 0) 118 | topConst?.priority = .defaultHigh 119 | leadConst = NSLayoutConstraint(item: imageView, attribute: .leading, relatedBy: .equal, toItem: topView, attribute: .leading, multiplier: 1, constant: 0) 120 | leadConst?.priority = .defaultHigh 121 | imageWidthConst = NSLayoutConstraint(item: imageView, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .width, multiplier: 1, constant: 1) 122 | imageWidthConst?.priority = .required 123 | imageHeightConst = NSLayoutConstraint(item: imageView, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .height, multiplier: 1, constant: 1) 124 | imageHeightConst?.priority = .required 125 | imageView.addConstraints([imageHeightConst!, imageWidthConst!]) 126 | topView.addConstraints([topConst!, leadConst!]) 127 | imageView.image = self.image 128 | 129 | // imageView gestures 130 | let pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(pinch)) 131 | imageView.addGestureRecognizer(pinchGesture) 132 | let panGesture = UIPanGestureRecognizer(target: self, action: #selector(pan)) 133 | imageView.addGestureRecognizer(panGesture) 134 | imageView.isUserInteractionEnabled = true 135 | 136 | //fade overlay 137 | fadeView.translatesAutoresizingMaskIntoConstraints = false 138 | fadeView.isUserInteractionEnabled = false 139 | fadeView.backgroundColor = UIColor.black.withAlphaComponent(0.3) 140 | topView.addSubview(fadeView) 141 | let horizontalFadeConst = NSLayoutConstraint.constraints(withVisualFormat: "H:|-(0)-[view]-(0)-|", options: NSLayoutConstraint.FormatOptions(), metrics: nil, views: ["view": fadeView]) 142 | let verticalFadeConst = NSLayoutConstraint.constraints(withVisualFormat: "V:|-(0)-[view]-(0)-|", options: NSLayoutConstraint.FormatOptions(), metrics: nil, views: ["view": fadeView]) 143 | topView.addConstraints(horizontalFadeConst + verticalFadeConst) 144 | 145 | // crop overlay 146 | cropView.translatesAutoresizingMaskIntoConstraints = false 147 | cropView.isUserInteractionEnabled = false 148 | topView.addSubview(cropView) 149 | let centerXConst = NSLayoutConstraint(item: cropView, attribute: .centerX, relatedBy: .equal, toItem: topView, attribute: .centerX, multiplier: 1, constant: 0) 150 | let centerYConst = NSLayoutConstraint(item: cropView, attribute: .centerY, relatedBy: .equal, toItem: topView, attribute: .centerY, multiplier: 1, constant: 0) 151 | let widthConst = NSLayoutConstraint(item: cropView, attribute: .width, relatedBy: .equal, toItem: topView, attribute: .width, multiplier: 0.9, constant: 0) 152 | widthConst.priority = .defaultHigh 153 | let heightConst = NSLayoutConstraint(item: cropView, attribute: .height, relatedBy: .lessThanOrEqual, toItem: topView, attribute: .height, multiplier: 0.9, constant: 0) 154 | let ratioConst = NSLayoutConstraint(item: cropView, attribute: .width, relatedBy: .equal, toItem: cropView, attribute: .height, multiplier: cropRatio, constant: 0) 155 | cropView.addConstraints([ratioConst]) 156 | topView.addConstraints([widthConst, heightConst, centerXConst, centerYConst]) 157 | cropView.layer.borderWidth = 1 158 | cropView.layer.borderColor = UIColor.white.cgColor 159 | cropView.backgroundColor = UIColor.clear 160 | 161 | // control buttons 162 | var cropCenterXMultiplier: CGFloat = 1.0 163 | if picker?.sourceType != .camera { //hide retake/cancel when using camera as camera has its own preview 164 | cancelButton.translatesAutoresizingMaskIntoConstraints = false 165 | cancelButton.setTitle(cancelButtonText, for: .normal) 166 | cancelButton.addTarget(self, action: #selector(cropCancel), for: .touchUpInside) 167 | bottomView.addSubview(cancelButton) 168 | let centerCancelXConst = NSLayoutConstraint(item: cancelButton, attribute: .centerX, relatedBy: .equal, toItem: bottomView, attribute: .centerX, multiplier: 0.5, constant: 0) 169 | let centerCancelYConst = NSLayoutConstraint(item: cancelButton, attribute: .centerY, relatedBy: .equal, toItem: bottomView, attribute: .centerY, multiplier: 1, constant: 0) 170 | bottomView.addConstraints([centerCancelXConst, centerCancelYConst]) 171 | cropCenterXMultiplier = 1.5 172 | } 173 | cropButton.translatesAutoresizingMaskIntoConstraints = false 174 | cropButton.addTarget(self, action: #selector(cropDone), for: .touchUpInside) 175 | bottomView.addSubview(cropButton) 176 | let centerCropXConst = NSLayoutConstraint(item: cropButton, attribute: .centerX, relatedBy: .equal, toItem: bottomView, attribute: .centerX, multiplier: cropCenterXMultiplier, constant: 0) 177 | let centerCropYConst = NSLayoutConstraint(item: cropButton, attribute: .centerY, relatedBy: .equal, toItem: bottomView, attribute: .centerY, multiplier: 1, constant: 0) 178 | bottomView.addConstraints([centerCropXConst, centerCropYConst]) 179 | 180 | self.view.bringSubviewToFront(bottomView) 181 | 182 | bottomView.layoutIfNeeded() 183 | topView.layoutIfNeeded() 184 | } 185 | 186 | override public func viewWillAppear(_ animated: Bool) { 187 | super.viewWillAppear(animated) 188 | 189 | self.cancelButton.setTitle(cancelButtonText, for: .normal) 190 | self.cropButton.setTitle(cropButtonText, for: .normal) 191 | 192 | if image == nil { 193 | self.dismiss(animated: true, completion: nil) 194 | } 195 | } 196 | 197 | override public func viewDidLayoutSubviews() { 198 | super.viewDidLayoutSubviews() 199 | guard !layoutDone else { 200 | return 201 | } 202 | layoutDone = true 203 | 204 | if ratio < 1 { 205 | imageWidthConst?.constant = cropView.frame.height / ratio 206 | imageHeightConst?.constant = cropView.frame.height 207 | } else { 208 | imageWidthConst?.constant = cropView.frame.width 209 | imageHeightConst?.constant = cropView.frame.width * ratio 210 | } 211 | 212 | let horizontal = NSLayoutConstraint.constraints(withVisualFormat: "H:|-(<=\(cropView.frame.origin.x))-[view]-(<=\(cropView.frame.origin.x))-|", options: NSLayoutConstraint.FormatOptions(), metrics: nil, views: ["view": imageView]) 213 | let vertical = NSLayoutConstraint.constraints(withVisualFormat: "V:|-(<=\(cropView.frame.origin.y))-[view]-(<=\(cropView.frame.origin.y))-|", options: NSLayoutConstraint.FormatOptions(), metrics: nil, views: ["view": imageView]) 214 | topView.addConstraints(horizontal + vertical) 215 | 216 | maskFadeView() 217 | orgWidth = imageWidthConst!.constant 218 | orgHeight = imageHeightConst!.constant 219 | } 220 | 221 | private func maskFadeView() { 222 | let path = UIBezierPath(rect: cropView.frame) 223 | path.append(UIBezierPath(rect: fadeView.frame)) 224 | let mask = CAShapeLayer() 225 | mask.fillRule = CAShapeLayerFillRule.evenOdd 226 | mask.path = path.cgPath 227 | fadeView.layer.mask = mask 228 | } 229 | 230 | //MARK: - button actions 231 | @objc func cropDone() { 232 | presenting = false 233 | if picker == nil { 234 | self.dismiss(animated: false, completion: { 235 | if self.autoClosePicker { 236 | self.picker?.dismiss(animated: true, completion: nil) 237 | } 238 | self.delegate?.didCropImage(originalImage: self.image, croppedImage: self.cropImage) 239 | }) 240 | } else { 241 | self.endAppearanceTransition() 242 | self.view.removeFromSuperview() 243 | self.removeFromParent() 244 | if self.autoClosePicker { 245 | self.picker?.dismiss(animated: true, completion: nil) 246 | } 247 | self.delegate?.didCropImage(originalImage: self.image, croppedImage: self.cropImage) 248 | } 249 | } 250 | 251 | @objc func cropCancel() { 252 | presenting = false 253 | if picker == nil { 254 | self.dismiss(animated: true, completion: nil) 255 | } else { 256 | self.endAppearanceTransition() 257 | self.view.removeFromSuperview() 258 | self.removeFromParent() 259 | } 260 | } 261 | 262 | //MARK: - gesture handling 263 | @objc func pinch(_ pinch: UIPinchGestureRecognizer) { 264 | if pinch.state == .began { 265 | orgWidth = imageWidthConst!.constant 266 | orgHeight = imageHeightConst!.constant 267 | pinchStart = pinch.location(in: self.view) 268 | } 269 | let scale = pinch.scale 270 | let height = max(orgHeight * scale, cropView.frame.height) 271 | let width = max(orgWidth * scale, cropView.frame.height / ratio) 272 | imageHeightConst?.constant = height 273 | imageWidthConst?.constant = width 274 | } 275 | 276 | @objc func pan(_ pan: UIPanGestureRecognizer) { 277 | if pan.state == .began { 278 | topStart = topConst!.constant 279 | leadStart = leadConst!.constant 280 | } 281 | let trans = pan.translation(in: self.view) 282 | leadConst?.constant = leadStart + trans.x 283 | topConst?.constant = topStart + trans.y 284 | } 285 | 286 | //MARK: - cropping done here 287 | private func crop() -> UIImage? { 288 | guard let image = self.image else { 289 | return nil 290 | } 291 | let imageSize = image.size 292 | let width = cropView.frame.width / imageView.frame.width 293 | let height = cropView.frame.height / imageView.frame.height 294 | let x = (cropView.frame.origin.x - imageView.frame.origin.x) / imageView.frame.width 295 | let y = (cropView.frame.origin.y - imageView.frame.origin.y) / imageView.frame.height 296 | 297 | let cropFrame = CGRect(x: x * imageSize.width, y: y * imageSize.height, width: imageSize.width * width, height: imageSize.height * height) 298 | if let cropCGImage = image.cgImage?.cropping(to: cropFrame) { 299 | let cropImage = UIImage(cgImage: cropCGImage, scale: 1, orientation: .up) 300 | return cropImage 301 | } 302 | return nil 303 | } 304 | 305 | //MARK: - UIImagePickerControllerDelegates 306 | public func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { 307 | presenting = false 308 | if delegate?.didCancel?() == nil { 309 | picker.dismiss(animated: true, completion: nil) 310 | } 311 | } 312 | 313 | var presenting = false 314 | 315 | public func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { 316 | guard !presenting else { 317 | return 318 | } 319 | guard let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage else { 320 | return 321 | } 322 | layoutDone = false 323 | presenting = true 324 | self.image = image.fixOrientation() 325 | self.picker?.view.addSubview(self.view) 326 | self.view.constraintToFill(superView: self.picker?.view) 327 | self.picker?.addChild(self) 328 | self.willMove(toParent: self.picker) 329 | self.beginAppearanceTransition(true, animated: false) 330 | } 331 | 332 | } 333 | 334 | extension UIView { 335 | func constraintToFill(superView view: UIView?) { 336 | guard let view = view else { 337 | assertionFailure("superview is nil") 338 | return 339 | } 340 | self.translatesAutoresizingMaskIntoConstraints = false 341 | self.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true 342 | self.heightAnchor.constraint(equalTo: view.heightAnchor).isActive = true 343 | self.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true 344 | self.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true 345 | } 346 | } 347 | --------------------------------------------------------------------------------