├── .gitignore ├── ImageCropper ├── ImageCropper.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ ├── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcuserdata │ │ │ ├── zhonguncle.xcuserdatad │ │ │ └── UserInterfaceState.xcuserstate │ │ │ └── zhongyijiang.xcuserdatad │ │ │ └── UserInterfaceState.xcuserstate │ └── xcuserdata │ │ ├── zhonguncle.xcuserdatad │ │ └── xcschemes │ │ │ └── xcschememanagement.plist │ │ └── zhongyijiang.xcuserdatad │ │ ├── xcdebugger │ │ └── Breakpoints_v2.xcbkptlist │ │ └── xcschemes │ │ └── xcschememanagement.plist └── ImageCropper │ ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── Contents.json │ └── image.imageset │ │ ├── 5BF3961F-E302-4186-90AE-DC2374E9C348_1_105_c.jpeg │ │ └── Contents.json │ ├── ContentView.swift │ ├── CropImage.swift │ ├── CropperView.swift │ ├── ImageCropperApp.swift │ └── NaviBarView.swift ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | -------------------------------------------------------------------------------- /ImageCropper/ImageCropper.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 55; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 6251C05C279B1A4D00B4C1C6 /* CropImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6251C05B279B1A4D00B4C1C6 /* CropImage.swift */; }; 11 | 6287F1572A7E49830070DDCB /* NaviBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6287F1562A7E49830070DDCB /* NaviBarView.swift */; }; 12 | 62FECADC2798E05400F77B4B /* ImageCropperApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62FECADB2798E05400F77B4B /* ImageCropperApp.swift */; }; 13 | 62FECADE2798E05400F77B4B /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62FECADD2798E05400F77B4B /* ContentView.swift */; }; 14 | 62FECAE02798E05500F77B4B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 62FECADF2798E05500F77B4B /* Assets.xcassets */; }; 15 | 62FECB0427999BAA00F77B4B /* CropperView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62FECB0327999BAA00F77B4B /* CropperView.swift */; }; 16 | /* End PBXBuildFile section */ 17 | 18 | /* Begin PBXFileReference section */ 19 | 6251C05B279B1A4D00B4C1C6 /* CropImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CropImage.swift; sourceTree = ""; }; 20 | 6287F1562A7E49830070DDCB /* NaviBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NaviBarView.swift; sourceTree = ""; }; 21 | 62FECAD82798E05400F77B4B /* ImageCropper.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ImageCropper.app; sourceTree = BUILT_PRODUCTS_DIR; }; 22 | 62FECADB2798E05400F77B4B /* ImageCropperApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCropperApp.swift; sourceTree = ""; }; 23 | 62FECADD2798E05400F77B4B /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 24 | 62FECADF2798E05500F77B4B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 25 | 62FECB0327999BAA00F77B4B /* CropperView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CropperView.swift; sourceTree = ""; }; 26 | /* End PBXFileReference section */ 27 | 28 | /* Begin PBXFrameworksBuildPhase section */ 29 | 62FECAD52798E05400F77B4B /* Frameworks */ = { 30 | isa = PBXFrameworksBuildPhase; 31 | buildActionMask = 2147483647; 32 | files = ( 33 | ); 34 | runOnlyForDeploymentPostprocessing = 0; 35 | }; 36 | /* End PBXFrameworksBuildPhase section */ 37 | 38 | /* Begin PBXGroup section */ 39 | 62FECACF2798E05400F77B4B = { 40 | isa = PBXGroup; 41 | children = ( 42 | 62FECADA2798E05400F77B4B /* ImageCropper */, 43 | 62FECAD92798E05400F77B4B /* Products */, 44 | ); 45 | sourceTree = ""; 46 | }; 47 | 62FECAD92798E05400F77B4B /* Products */ = { 48 | isa = PBXGroup; 49 | children = ( 50 | 62FECAD82798E05400F77B4B /* ImageCropper.app */, 51 | ); 52 | name = Products; 53 | sourceTree = ""; 54 | }; 55 | 62FECADA2798E05400F77B4B /* ImageCropper */ = { 56 | isa = PBXGroup; 57 | children = ( 58 | 62FECADB2798E05400F77B4B /* ImageCropperApp.swift */, 59 | 62FECADD2798E05400F77B4B /* ContentView.swift */, 60 | 62FECB0327999BAA00F77B4B /* CropperView.swift */, 61 | 6287F1562A7E49830070DDCB /* NaviBarView.swift */, 62 | 6251C05B279B1A4D00B4C1C6 /* CropImage.swift */, 63 | 62FECADF2798E05500F77B4B /* Assets.xcassets */, 64 | ); 65 | path = ImageCropper; 66 | sourceTree = ""; 67 | }; 68 | /* End PBXGroup section */ 69 | 70 | /* Begin PBXNativeTarget section */ 71 | 62FECAD72798E05400F77B4B /* ImageCropper */ = { 72 | isa = PBXNativeTarget; 73 | buildConfigurationList = 62FECAE62798E05500F77B4B /* Build configuration list for PBXNativeTarget "ImageCropper" */; 74 | buildPhases = ( 75 | 62FECAD42798E05400F77B4B /* Sources */, 76 | 62FECAD52798E05400F77B4B /* Frameworks */, 77 | 62FECAD62798E05400F77B4B /* Resources */, 78 | ); 79 | buildRules = ( 80 | ); 81 | dependencies = ( 82 | ); 83 | name = ImageCropper; 84 | productName = ImageCropper; 85 | productReference = 62FECAD82798E05400F77B4B /* ImageCropper.app */; 86 | productType = "com.apple.product-type.application"; 87 | }; 88 | /* End PBXNativeTarget section */ 89 | 90 | /* Begin PBXProject section */ 91 | 62FECAD02798E05400F77B4B /* Project object */ = { 92 | isa = PBXProject; 93 | attributes = { 94 | BuildIndependentTargetsInParallel = 1; 95 | LastSwiftUpdateCheck = 1320; 96 | LastUpgradeCheck = 1530; 97 | TargetAttributes = { 98 | 62FECAD72798E05400F77B4B = { 99 | CreatedOnToolsVersion = 13.2.1; 100 | }; 101 | }; 102 | }; 103 | buildConfigurationList = 62FECAD32798E05400F77B4B /* Build configuration list for PBXProject "ImageCropper" */; 104 | compatibilityVersion = "Xcode 13.0"; 105 | developmentRegion = en; 106 | hasScannedForEncodings = 0; 107 | knownRegions = ( 108 | en, 109 | Base, 110 | ); 111 | mainGroup = 62FECACF2798E05400F77B4B; 112 | productRefGroup = 62FECAD92798E05400F77B4B /* Products */; 113 | projectDirPath = ""; 114 | projectRoot = ""; 115 | targets = ( 116 | 62FECAD72798E05400F77B4B /* ImageCropper */, 117 | ); 118 | }; 119 | /* End PBXProject section */ 120 | 121 | /* Begin PBXResourcesBuildPhase section */ 122 | 62FECAD62798E05400F77B4B /* Resources */ = { 123 | isa = PBXResourcesBuildPhase; 124 | buildActionMask = 2147483647; 125 | files = ( 126 | 62FECAE02798E05500F77B4B /* Assets.xcassets in Resources */, 127 | ); 128 | runOnlyForDeploymentPostprocessing = 0; 129 | }; 130 | /* End PBXResourcesBuildPhase section */ 131 | 132 | /* Begin PBXSourcesBuildPhase section */ 133 | 62FECAD42798E05400F77B4B /* Sources */ = { 134 | isa = PBXSourcesBuildPhase; 135 | buildActionMask = 2147483647; 136 | files = ( 137 | 6287F1572A7E49830070DDCB /* NaviBarView.swift in Sources */, 138 | 62FECADE2798E05400F77B4B /* ContentView.swift in Sources */, 139 | 6251C05C279B1A4D00B4C1C6 /* CropImage.swift in Sources */, 140 | 62FECB0427999BAA00F77B4B /* CropperView.swift in Sources */, 141 | 62FECADC2798E05400F77B4B /* ImageCropperApp.swift in Sources */, 142 | ); 143 | runOnlyForDeploymentPostprocessing = 0; 144 | }; 145 | /* End PBXSourcesBuildPhase section */ 146 | 147 | /* Begin XCBuildConfiguration section */ 148 | 62FECAE42798E05500F77B4B /* Debug */ = { 149 | isa = XCBuildConfiguration; 150 | buildSettings = { 151 | ALWAYS_SEARCH_USER_PATHS = NO; 152 | CLANG_ANALYZER_NONNULL = YES; 153 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 154 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 155 | CLANG_CXX_LIBRARY = "libc++"; 156 | CLANG_ENABLE_MODULES = YES; 157 | CLANG_ENABLE_OBJC_ARC = YES; 158 | CLANG_ENABLE_OBJC_WEAK = YES; 159 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 160 | CLANG_WARN_BOOL_CONVERSION = YES; 161 | CLANG_WARN_COMMA = YES; 162 | CLANG_WARN_CONSTANT_CONVERSION = YES; 163 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 164 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 165 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 166 | CLANG_WARN_EMPTY_BODY = YES; 167 | CLANG_WARN_ENUM_CONVERSION = YES; 168 | CLANG_WARN_INFINITE_RECURSION = YES; 169 | CLANG_WARN_INT_CONVERSION = YES; 170 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 171 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 172 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 173 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 174 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 175 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 176 | CLANG_WARN_STRICT_PROTOTYPES = YES; 177 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 178 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 179 | CLANG_WARN_UNREACHABLE_CODE = YES; 180 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 181 | COPY_PHASE_STRIP = NO; 182 | DEBUG_INFORMATION_FORMAT = dwarf; 183 | ENABLE_STRICT_OBJC_MSGSEND = YES; 184 | ENABLE_TESTABILITY = YES; 185 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 186 | GCC_C_LANGUAGE_STANDARD = gnu11; 187 | GCC_DYNAMIC_NO_PIC = NO; 188 | GCC_NO_COMMON_BLOCKS = YES; 189 | GCC_OPTIMIZATION_LEVEL = 0; 190 | GCC_PREPROCESSOR_DEFINITIONS = ( 191 | "DEBUG=1", 192 | "$(inherited)", 193 | ); 194 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 195 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 196 | GCC_WARN_UNDECLARED_SELECTOR = YES; 197 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 198 | GCC_WARN_UNUSED_FUNCTION = YES; 199 | GCC_WARN_UNUSED_VARIABLE = YES; 200 | IPHONEOS_DEPLOYMENT_TARGET = 15.2; 201 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 202 | MTL_FAST_MATH = YES; 203 | ONLY_ACTIVE_ARCH = YES; 204 | SDKROOT = iphoneos; 205 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 206 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 207 | }; 208 | name = Debug; 209 | }; 210 | 62FECAE52798E05500F77B4B /* Release */ = { 211 | isa = XCBuildConfiguration; 212 | buildSettings = { 213 | ALWAYS_SEARCH_USER_PATHS = NO; 214 | CLANG_ANALYZER_NONNULL = YES; 215 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 216 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 217 | CLANG_CXX_LIBRARY = "libc++"; 218 | CLANG_ENABLE_MODULES = YES; 219 | CLANG_ENABLE_OBJC_ARC = YES; 220 | CLANG_ENABLE_OBJC_WEAK = YES; 221 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 222 | CLANG_WARN_BOOL_CONVERSION = YES; 223 | CLANG_WARN_COMMA = YES; 224 | CLANG_WARN_CONSTANT_CONVERSION = YES; 225 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 226 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 227 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 228 | CLANG_WARN_EMPTY_BODY = YES; 229 | CLANG_WARN_ENUM_CONVERSION = YES; 230 | CLANG_WARN_INFINITE_RECURSION = YES; 231 | CLANG_WARN_INT_CONVERSION = YES; 232 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 233 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 234 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 235 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 236 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 237 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 238 | CLANG_WARN_STRICT_PROTOTYPES = YES; 239 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 240 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 241 | CLANG_WARN_UNREACHABLE_CODE = YES; 242 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 243 | COPY_PHASE_STRIP = NO; 244 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 245 | ENABLE_NS_ASSERTIONS = NO; 246 | ENABLE_STRICT_OBJC_MSGSEND = YES; 247 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 248 | GCC_C_LANGUAGE_STANDARD = gnu11; 249 | GCC_NO_COMMON_BLOCKS = YES; 250 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 251 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 252 | GCC_WARN_UNDECLARED_SELECTOR = YES; 253 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 254 | GCC_WARN_UNUSED_FUNCTION = YES; 255 | GCC_WARN_UNUSED_VARIABLE = YES; 256 | IPHONEOS_DEPLOYMENT_TARGET = 15.2; 257 | MTL_ENABLE_DEBUG_INFO = NO; 258 | MTL_FAST_MATH = YES; 259 | SDKROOT = iphoneos; 260 | SWIFT_COMPILATION_MODE = wholemodule; 261 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 262 | VALIDATE_PRODUCT = YES; 263 | }; 264 | name = Release; 265 | }; 266 | 62FECAE72798E05500F77B4B /* Debug */ = { 267 | isa = XCBuildConfiguration; 268 | buildSettings = { 269 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 270 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 271 | CODE_SIGN_STYLE = Automatic; 272 | CURRENT_PROJECT_VERSION = 1; 273 | DEVELOPMENT_ASSET_PATHS = ImageCropper/Assets.xcassets; 274 | DEVELOPMENT_TEAM = 96V8RMT7SR; 275 | ENABLE_PREVIEWS = YES; 276 | GENERATE_INFOPLIST_FILE = YES; 277 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 278 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 279 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 280 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 281 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 282 | LD_RUNPATH_SEARCH_PATHS = ( 283 | "$(inherited)", 284 | "@executable_path/Frameworks", 285 | ); 286 | MARKETING_VERSION = 1.0; 287 | PRODUCT_BUNDLE_IDENTIFIER = Package.ImageCropper; 288 | PRODUCT_NAME = "$(TARGET_NAME)"; 289 | SWIFT_EMIT_LOC_STRINGS = YES; 290 | SWIFT_VERSION = 5.0; 291 | TARGETED_DEVICE_FAMILY = "1,2"; 292 | }; 293 | name = Debug; 294 | }; 295 | 62FECAE82798E05500F77B4B /* Release */ = { 296 | isa = XCBuildConfiguration; 297 | buildSettings = { 298 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 299 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 300 | CODE_SIGN_STYLE = Automatic; 301 | CURRENT_PROJECT_VERSION = 1; 302 | DEVELOPMENT_ASSET_PATHS = ImageCropper/Assets.xcassets; 303 | DEVELOPMENT_TEAM = 96V8RMT7SR; 304 | ENABLE_PREVIEWS = YES; 305 | GENERATE_INFOPLIST_FILE = YES; 306 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 307 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 308 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 309 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 310 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 311 | LD_RUNPATH_SEARCH_PATHS = ( 312 | "$(inherited)", 313 | "@executable_path/Frameworks", 314 | ); 315 | MARKETING_VERSION = 1.0; 316 | PRODUCT_BUNDLE_IDENTIFIER = Package.ImageCropper; 317 | PRODUCT_NAME = "$(TARGET_NAME)"; 318 | SWIFT_EMIT_LOC_STRINGS = YES; 319 | SWIFT_VERSION = 5.0; 320 | TARGETED_DEVICE_FAMILY = "1,2"; 321 | }; 322 | name = Release; 323 | }; 324 | /* End XCBuildConfiguration section */ 325 | 326 | /* Begin XCConfigurationList section */ 327 | 62FECAD32798E05400F77B4B /* Build configuration list for PBXProject "ImageCropper" */ = { 328 | isa = XCConfigurationList; 329 | buildConfigurations = ( 330 | 62FECAE42798E05500F77B4B /* Debug */, 331 | 62FECAE52798E05500F77B4B /* Release */, 332 | ); 333 | defaultConfigurationIsVisible = 0; 334 | defaultConfigurationName = Release; 335 | }; 336 | 62FECAE62798E05500F77B4B /* Build configuration list for PBXNativeTarget "ImageCropper" */ = { 337 | isa = XCConfigurationList; 338 | buildConfigurations = ( 339 | 62FECAE72798E05500F77B4B /* Debug */, 340 | 62FECAE82798E05500F77B4B /* Release */, 341 | ); 342 | defaultConfigurationIsVisible = 0; 343 | defaultConfigurationName = Release; 344 | }; 345 | /* End XCConfigurationList section */ 346 | }; 347 | rootObject = 62FECAD02798E05400F77B4B /* Project object */; 348 | } 349 | -------------------------------------------------------------------------------- /ImageCropper/ImageCropper.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ImageCropper/ImageCropper.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ImageCropper/ImageCropper.xcodeproj/project.xcworkspace/xcuserdata/zhonguncle.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZhongUncle/SwiftUI_ImageCropper/9f1a6b583f2db7d0d4815b78a3e514a07b08c33f/ImageCropper/ImageCropper.xcodeproj/project.xcworkspace/xcuserdata/zhonguncle.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /ImageCropper/ImageCropper.xcodeproj/project.xcworkspace/xcuserdata/zhongyijiang.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZhongUncle/SwiftUI_ImageCropper/9f1a6b583f2db7d0d4815b78a3e514a07b08c33f/ImageCropper/ImageCropper.xcodeproj/project.xcworkspace/xcuserdata/zhongyijiang.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /ImageCropper/ImageCropper.xcodeproj/xcuserdata/zhonguncle.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | ImageCropper.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /ImageCropper/ImageCropper.xcodeproj/xcuserdata/zhongyijiang.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /ImageCropper/ImageCropper.xcodeproj/xcuserdata/zhongyijiang.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | ImageCropper.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /ImageCropper/ImageCropper/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /ImageCropper/ImageCropper/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /ImageCropper/ImageCropper/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /ImageCropper/ImageCropper/Assets.xcassets/image.imageset/5BF3961F-E302-4186-90AE-DC2374E9C348_1_105_c.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZhongUncle/SwiftUI_ImageCropper/9f1a6b583f2db7d0d4815b78a3e514a07b08c33f/ImageCropper/ImageCropper/Assets.xcassets/image.imageset/5BF3961F-E302-4186-90AE-DC2374E9C348_1_105_c.jpeg -------------------------------------------------------------------------------- /ImageCropper/ImageCropper/Assets.xcassets/image.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "5BF3961F-E302-4186-90AE-DC2374E9C348_1_105_c.jpeg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ImageCropper/ImageCropper/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // ImageCropper 4 | // 5 | // Created by 钟宜江 on 2022/1/20. 6 | // 7 | 8 | import SwiftUI 9 | 10 | 11 | struct ContentView: View { 12 | var inputimage = UIImage(named: "image")! 13 | @State private var croppedImage: UIImage = UIImage() 14 | @State private var showCropper = false 15 | 16 | @State private var screenWidth = UIScreen.main.bounds.width 17 | @State private var screenHeight = UIScreen.main.bounds.height 18 | 19 | var body: some View { 20 | if #available(iOS 16.0, *) { 21 | NavigationStack { 22 | VStack { 23 | Text("This is Test View") 24 | .font(.title) 25 | .bold() 26 | ZStack { 27 | // This will navigate into cropper view 28 | NavigationLink(destination: CropperView(inputImage: inputimage, croppedImage: $croppedImage),isActive: $showCropper) { EmptyView() } 29 | 30 | VStack { 31 | VStack(alignment: .leading) { 32 | Text("Origin Image") 33 | .font(.title) 34 | HStack { 35 | Image(uiImage: inputimage) 36 | .resizable() 37 | .scaledToFit() 38 | } 39 | 40 | 41 | Text("Cropped Image") 42 | .font(.title) 43 | HStack { 44 | Image(uiImage: croppedImage) 45 | .resizable() 46 | .scaledToFit() 47 | .frame(width: screenWidth * 0.6, height: screenHeight/5) 48 | } 49 | } 50 | Button (action : { 51 | showCropper = true 52 | }) { 53 | Text("Go to Crop") 54 | .padding(.all, 10) 55 | .background(Color.blue) 56 | .foregroundColor(.white) 57 | .shadow(color: .gray, radius: 1) 58 | .padding(.top, 50) 59 | } 60 | } 61 | } 62 | .padding() 63 | } 64 | } 65 | } else { 66 | // Fallback on earlier versions 67 | NavigationView { 68 | VStack { 69 | Text("This is Test View") 70 | .font(.title) 71 | .bold() 72 | ZStack { 73 | // This will navigate into cropper view 74 | NavigationLink(destination: CropperView(inputImage: inputimage, croppedImage: $croppedImage),isActive: $showCropper) { EmptyView() } 75 | 76 | VStack { 77 | Text("Origin Image") 78 | Image(uiImage: inputimage) 79 | .resizable() 80 | .scaledToFit() 81 | 82 | Text("Cropped Image") 83 | Image(uiImage: croppedImage) 84 | .resizable() 85 | .scaledToFit() 86 | .frame(width: screenWidth * 0.6, height: screenHeight/5) 87 | 88 | HStack { 89 | Button (action : { 90 | showCropper = true 91 | }) { 92 | Text("Go to Crop") 93 | .padding(.all, 10) 94 | .background(Color.blue) 95 | .foregroundColor(.white) 96 | .shadow(color: .gray, radius: 1) 97 | .padding(.top, 50) 98 | } 99 | } 100 | } 101 | } 102 | .padding() 103 | } 104 | } 105 | } 106 | } 107 | } 108 | 109 | 110 | struct ContentView_Previews: PreviewProvider { 111 | static var previews: some View { 112 | ContentView() 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /ImageCropper/ImageCropper/CropImage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CropImage.swift 3 | // ImageCropper 4 | // 5 | // Created by 钟宜江 on 2022/1/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // This function for crop givem image 11 | func cropImage(_ inputImage: UIImage, toRect cropRect: CGRect, viewWidth: CGFloat, viewHeight: CGFloat) -> UIImage? 12 | { 13 | let imageViewWidthScale = inputImage.size.width / viewWidth 14 | let imageViewHeightScale = inputImage.size.height / viewHeight 15 | // Scale cropRect to handle images larger than shown-on-screen size 16 | let cropZone = CGRect(x:cropRect.origin.x * imageViewWidthScale, 17 | y:cropRect.origin.y * imageViewHeightScale, 18 | width:cropRect.size.width * imageViewWidthScale, 19 | height:cropRect.size.height * imageViewHeightScale) 20 | // Perform cropping in Core Graphics 21 | guard let cutImageRef: CGImage = inputImage.cgImage?.cropping(to:cropZone) 22 | else { 23 | return nil 24 | } 25 | // Return image to UIImage 26 | let croppedImage: UIImage = UIImage(cgImage: cutImageRef) 27 | return croppedImage 28 | } 29 | -------------------------------------------------------------------------------- /ImageCropper/ImageCropper/CropperView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CropperView.swift 3 | // ImageCropper 4 | // 5 | // Created by 钟宜江 on 2022/1/20. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct CropperView: View { 11 | var inputImage: UIImage 12 | @Binding var croppedImage: UIImage 13 | 14 | //视图的宽高,不用全局变量是为了方便修改视图:默认横向(landscape) 15 | @State private var screenWidth = UIScreen.main.bounds.height 16 | @State private var screenHeight = UIScreen.main.bounds.width 17 | 18 | //后面可以变成自定义的部分 19 | //边框颜色 20 | var cropBorderColor: Color? = Color.white 21 | //顶点图案颜色 22 | var cropVerticesColor: Color = Color.pink 23 | //遮罩透明度 24 | var cropperOutsideOpacity: Double = 0.4 25 | // //裁切框样式 26 | // @State private var iconVertices: Bool = false 27 | 28 | 29 | @Environment(\.presentationMode) private var presentationMode: Binding 30 | 31 | @State private var imageDisplayWidth: CGFloat = 0 32 | @State private var imageDisplayHeight: CGFloat = 0 33 | 34 | @State private var cropWidth: CGFloat = UIScreen.main.bounds.height/3 35 | @State private var cropHeight: CGFloat = UIScreen.main.bounds.height/3*0.5 36 | @State private var cropWidthAdd: CGFloat = 0 37 | @State private var cropHeightAdd: CGFloat = 0 38 | @State private var cropMode = 0 39 | 40 | @State private var currentPositionZS: CGSize = .zero 41 | @State private var newPositionZS: CGSize = .zero 42 | 43 | @State private var currentPositionZ: CGSize = .zero 44 | @State private var newPositionZ: CGSize = .zero 45 | 46 | @State private var currentPositionZX: CGSize = .zero 47 | @State private var newPositionZX: CGSize = .zero 48 | 49 | @State private var currentPositionX: CGSize = .zero 50 | @State private var newPositionX: CGSize = .zero 51 | 52 | @State private var currentPositionYX: CGSize = .zero 53 | @State private var newPositionYX: CGSize = .zero 54 | 55 | @State private var currentPositionY: CGSize = .zero 56 | @State private var newPositionY: CGSize = .zero 57 | 58 | @State private var currentPositionYS: CGSize = .zero 59 | @State private var newPositionYS: CGSize = .zero 60 | 61 | @State private var currentPositionS: CGSize = .zero 62 | @State private var newPositionS: CGSize = .zero 63 | 64 | @State private var currentPositionCrop: CGSize = .zero 65 | @State private var newPositionCrop: CGSize = .zero 66 | 67 | var body: some View { 68 | ZStack { 69 | //黑色背景 70 | Rectangle() 71 | .ignoresSafeArea() 72 | .foregroundColor(.black) 73 | 74 | VStack { 75 | //NaviBar 76 | ZStack(alignment: .leading) { 77 | Rectangle() 78 | .frame(height: (UIDevice.current.model == "iPhone") ? screenHeight/5 : screenHeight/15) 79 | .foregroundColor(Color(red: 0.1, green: 0.1, blue: 0.1)) 80 | 81 | VStack { 82 | Button(action: { 83 | self.presentationMode.wrappedValue.dismiss() 84 | }, label: { 85 | Image(systemName: "chevron.backward") 86 | .padding() 87 | .foregroundColor(Color.white) 88 | }) 89 | } 90 | .offset(y: (UIDevice.current.model == "iPhone") ? 10 : 5) 91 | } 92 | .ignoresSafeArea() 93 | 94 | ZStack { 95 | ZStack { 96 | Image(uiImage: inputImage) 97 | .resizable() 98 | .scaledToFit() 99 | .overlay(GeometryReader{geo -> AnyView in 100 | DispatchQueue.main.async{ 101 | self.imageDisplayWidth = geo.size.width 102 | self.imageDisplayHeight = geo.size.height 103 | } 104 | return AnyView(EmptyView()) 105 | }) 106 | 107 | ZStack { 108 | //外围半透明遮罩 109 | //左边 110 | Rectangle() 111 | .foregroundColor(.black) 112 | .opacity(cropperOutsideOpacity) 113 | .frame(width: (imageDisplayWidth/2 - (cropWidth/2 - currentPositionCrop.width + cropWidthAdd/2))) 114 | .offset(x: -imageDisplayWidth/2 + (imageDisplayWidth/2 - (cropWidth/2 - currentPositionCrop.width + cropWidthAdd/2))/2) 115 | //右边 116 | Rectangle() 117 | .foregroundColor(.black) 118 | .opacity(cropperOutsideOpacity) 119 | .frame(width: imageDisplayWidth/2 - (cropWidth/2 + currentPositionCrop.width + cropWidthAdd/2)) 120 | .offset(x: imageDisplayWidth/2 - (imageDisplayWidth/2 - (cropWidth/2 + currentPositionCrop.width + cropWidthAdd/2))/2) 121 | //上边 122 | Rectangle() 123 | .foregroundColor(.black) 124 | .opacity(cropperOutsideOpacity) 125 | .frame(width: cropWidth + cropWidthAdd, height: imageDisplayHeight/2 - (cropHeight/2 - currentPositionCrop.height + cropHeightAdd/2)) 126 | .offset(x: currentPositionCrop.width, y: -imageDisplayHeight/2 + (imageDisplayHeight/2 - (cropHeight/2 - currentPositionCrop.height + cropHeightAdd/2))/2) 127 | //下边 128 | Rectangle() 129 | .foregroundColor(.black) 130 | .opacity(cropperOutsideOpacity) 131 | .frame(width: cropWidth + cropWidthAdd, height: imageDisplayHeight/2 - (cropHeight/2 + currentPositionCrop.height + cropHeightAdd/2)) 132 | .offset(x: currentPositionCrop.width, y: imageDisplayHeight/2 - (imageDisplayHeight/2 - (cropHeight/2 + currentPositionCrop.height + cropHeightAdd/2))/2) 133 | 134 | //裁剪框 135 | Rectangle() 136 | .fill(Color.white.opacity(0.01)) 137 | .frame(width: cropWidth+cropWidthAdd, height: cropHeight+cropHeightAdd) 138 | .offset(x: currentPositionCrop.width, y: currentPositionCrop.height) 139 | .gesture( 140 | DragGesture() 141 | .onChanged { value in 142 | //这里的newPosition表示的是当前偏移量,因为我们是通过偏移量来控制表示位置的。直接操作currentPosition会导致递归相加,这不是我们想要的。 143 | //让currentPosition 等于 新增偏移量 和 newPosition 相加,这样可以避免递归 144 | //max和min用于防止出界 145 | currentPositionCrop.width = min(max(value.translation.width + newPositionCrop.width, -imageDisplayWidth/2+cropWidth/2), imageDisplayWidth/2-cropWidth/2) 146 | currentPositionCrop.height = min(max(value.translation.height + newPositionCrop.height, -imageDisplayHeight/2+cropHeight/2), imageDisplayHeight/2-cropHeight/2) 147 | //各个角的坐标其实和Crop的一样,只是zs之类的的减去了一半crop的偏移量作为额外的偏移量 148 | currentPositionZS.width = currentPositionCrop.width 149 | currentPositionZS.height = currentPositionCrop.height 150 | currentPositionZX.width = currentPositionCrop.width 151 | currentPositionZX.height = currentPositionCrop.height 152 | currentPositionYX.width = currentPositionCrop.width 153 | currentPositionYX.height = currentPositionCrop.height 154 | currentPositionYS.width = currentPositionCrop.width 155 | currentPositionYS.height = currentPositionCrop.height 156 | 157 | currentPositionS.width = currentPositionCrop.width 158 | currentPositionS.height = currentPositionCrop.height 159 | currentPositionZ.width = currentPositionCrop.width 160 | currentPositionZ.height = currentPositionCrop.height 161 | currentPositionX.width = currentPositionCrop.width 162 | currentPositionX.height = currentPositionCrop.height 163 | currentPositionY.width = currentPositionCrop.width 164 | currentPositionY.height = currentPositionCrop.height 165 | } 166 | .onEnded { value in 167 | //移动结束后,让当前坐标的值等于之前的值加上的坐标 168 | currentPositionCrop.width = min(max(value.translation.width + newPositionCrop.width, -imageDisplayWidth/2+cropWidth/2), imageDisplayWidth/2-cropWidth/2) 169 | currentPositionCrop.height = min(max(value.translation.height + newPositionCrop.height, -imageDisplayHeight/2+cropHeight/2), imageDisplayHeight/2-cropHeight/2) 170 | //让new等于现在的坐标 171 | self.newPositionCrop = self.currentPositionCrop 172 | 173 | operateOnEnd() 174 | }) 175 | 176 | //Sides 177 | //Top 178 | Rectangle() 179 | .frame(width: cropWidth + cropWidthAdd, height: 2) 180 | .offset(x: currentPositionS.width, y: currentPositionS.height - cropHeight/2) 181 | .foregroundColor(cropBorderColor) 182 | .padding(.vertical) 183 | .gesture( 184 | DragGesture() 185 | .onChanged { value in 186 | var modeRatio:CGFloat = 1 187 | if cropMode == 0{ 188 | //现在的高度大于40并且不超过上边界 189 | if cropHeight-value.translation.height > 40 && value.translation.height + newPositionCrop.height >= -imageDisplayHeight/2+cropHeight/2{ 190 | //自由模式 191 | currentPositionS.height = value.translation.height + newPositionS.height 192 | //相邻的角 193 | currentPositionZS.height = value.translation.height + newPositionZS.height 194 | currentPositionYS.height = value.translation.height + newPositionYS.height 195 | //相邻的边 196 | currentPositionY.height = value.translation.height/2 + newPositionS.height 197 | currentPositionZ.height = value.translation.height/2 + newPositionZ.height 198 | 199 | currentPositionCrop.height = value.translation.height/2 + newPositionCrop.height 200 | cropHeightAdd = -value.translation.height 201 | } 202 | }else{ 203 | if cropMode == 1{ 204 | //16:9 205 | modeRatio = 16/9 206 | }else if cropMode == 2{ 207 | //4:3 208 | modeRatio = 4/3 209 | } 210 | //现在的高度和宽度都大于40 211 | if (cropHeight-value.translation.height > 40 || cropWidth-value.translation.height*modeRatio > 40) && 212 | //防止上边超过 213 | value.translation.height + newPositionS.height >= -imageDisplayHeight/2+cropHeight/2 && 214 | //防止下边超过 215 | value.translation.height + newPositionS.height <= imageDisplayHeight/2-cropHeight/2 && 216 | //防止右边超过 217 | currentPositionCrop.width+(cropWidth-value.translation.height*modeRatio)/2 <= imageDisplayWidth/2 && 218 | //防止左边超过 219 | currentPositionCrop.width-(cropWidth-value.translation.height*modeRatio)/2 >= -imageDisplayWidth/2 { 220 | currentPositionS.height = value.translation.height + newPositionS.height 221 | //相邻的角 222 | currentPositionZS.width = value.translation.height*modeRatio/2 + newPositionZS.width 223 | currentPositionZS.height = value.translation.height + newPositionZS.height 224 | currentPositionYS.width = -value.translation.height*modeRatio/2 + newPositionYS.width 225 | currentPositionYS.height = value.translation.height + newPositionYS.height 226 | //不相邻的角 227 | currentPositionZX.width = value.translation.height*modeRatio/2 + newPositionZX.width 228 | currentPositionYX.width = -value.translation.height*modeRatio/2 + newPositionYX.width 229 | //相邻的边 230 | currentPositionY.width = -value.translation.height*modeRatio/2 + newPositionY.width 231 | currentPositionY.height = value.translation.height/2 + newPositionY.height 232 | currentPositionZ.width = value.translation.height*modeRatio/2 + newPositionZ.width 233 | currentPositionZ.height = value.translation.height/2 + newPositionZ.height 234 | //裁剪器部分 235 | currentPositionCrop.height = value.translation.height/2 + newPositionCrop.height 236 | cropWidthAdd = -value.translation.height*modeRatio 237 | cropHeightAdd = -value.translation.height 238 | } 239 | } 240 | } 241 | .onEnded { value in 242 | operateOnEnd() 243 | } 244 | ) 245 | 246 | //Buttom 247 | Rectangle() 248 | .frame(width: cropWidth + cropWidthAdd, height: 2) 249 | .foregroundColor(cropBorderColor) 250 | .offset(x: currentPositionX.width, y: currentPositionX.height+cropHeight/2) 251 | .padding(.vertical) 252 | .gesture( 253 | DragGesture() 254 | .onChanged { value in 255 | var modeRatio:CGFloat = 1 256 | if cropMode == 0{ 257 | //现在的高度大于40并且不超过下边界 258 | if cropHeight+value.translation.height > 40 && value.translation.height + newPositionCrop.height <= imageDisplayHeight/2-cropHeight/2{ 259 | //自由模式 260 | currentPositionX.height = value.translation.height + newPositionX.height 261 | //相邻的角 262 | currentPositionZX.height = value.translation.height + newPositionZX.height 263 | currentPositionYX.height = value.translation.height + newPositionYX.height 264 | //相邻的边 265 | currentPositionY.height = value.translation.height/2 + newPositionY.height 266 | currentPositionZ.height = value.translation.height/2 + newPositionZ.height 267 | //裁剪器部分 268 | currentPositionCrop.height = value.translation.height/2 + newPositionCrop.height 269 | cropHeightAdd = value.translation.height 270 | } 271 | }else{ 272 | if cropMode == 1{ 273 | //16:9 274 | modeRatio = 16/9 275 | }else if cropMode == 2{ 276 | //4:3 277 | modeRatio = 4/3 278 | } 279 | if (cropHeight+value.translation.height > 40 || cropWidth+value.translation.height*modeRatio > 40) && 280 | //防止上边超过 281 | value.translation.height + newPositionX.height >= -imageDisplayHeight/2+cropHeight/2 && 282 | //防止下边超过 283 | value.translation.height + newPositionX.height <= imageDisplayHeight/2-cropHeight/2 && 284 | //防止右边超过 285 | currentPositionCrop.width+(cropWidth+value.translation.height*modeRatio)/2 <= imageDisplayWidth/2 && 286 | //防止左边超过 287 | currentPositionCrop.width-(cropWidth+value.translation.height*modeRatio)/2 >= -imageDisplayWidth/2 { 288 | 289 | //自由模式 290 | currentPositionX.height = value.translation.height + newPositionX.height 291 | //相邻的角 292 | currentPositionZX.width = -value.translation.height*modeRatio/2 + newPositionZX.width 293 | currentPositionZX.height = value.translation.height + newPositionZX.height 294 | currentPositionYX.width = value.translation.height*modeRatio/2 + newPositionYX.width 295 | currentPositionYX.height = value.translation.height + newPositionYX.height 296 | //不相邻的角 297 | currentPositionZS.width = -value.translation.height*modeRatio/2 + newPositionZS.width 298 | currentPositionYS.width = value.translation.height*modeRatio/2 + newPositionYS.width 299 | //相邻的边 300 | currentPositionY.width = value.translation.height*modeRatio/2 + newPositionY.width 301 | currentPositionY.height = value.translation.height/2 + newPositionY.height 302 | currentPositionZ.width = -value.translation.height*modeRatio/2 + newPositionZ.width 303 | currentPositionZ.height = value.translation.height/2 + newPositionZ.height 304 | //裁剪器部分 305 | currentPositionCrop.height = value.translation.height/2 + newPositionCrop.height 306 | cropWidthAdd = value.translation.height*modeRatio 307 | cropHeightAdd = value.translation.height 308 | } 309 | } 310 | } 311 | .onEnded { value in 312 | operateOnEnd() 313 | } 314 | ) 315 | 316 | //Leading 317 | Rectangle() 318 | .frame(width: 2, height: cropHeight + cropHeightAdd) 319 | .foregroundColor(cropBorderColor) 320 | .offset(x: currentPositionZ.width-cropWidth/2, y: currentPositionZ.height) 321 | .padding(.horizontal) 322 | .gesture( 323 | DragGesture() 324 | .onChanged { value in 325 | var modeRatio:CGFloat = 1.00 326 | if cropMode == 0{ 327 | //现在的宽度大于40并且不超过左边界 328 | if cropWidth-value.translation.width > 40 && value.translation.width + newPositionCrop.width >= -imageDisplayWidth/2+cropWidth/2{ 329 | //自由模式 330 | currentPositionZ.width = value.translation.width + newPositionZ.width 331 | //相邻的角 332 | currentPositionZS.width = value.translation.width + newPositionZS.width 333 | currentPositionZS.width = value.translation.width + newPositionZS.width 334 | currentPositionZX.width = value.translation.width + newPositionZX.width 335 | currentPositionZX.width = value.translation.width + newPositionZX.width 336 | //相邻的边 337 | currentPositionS.width = value.translation.width/2 + newPositionS.width 338 | currentPositionX.width = value.translation.width/2 + newPositionX.width 339 | //裁剪器部分 340 | currentPositionCrop.width = value.translation.width/2 + newPositionCrop.width 341 | cropWidthAdd = -value.translation.width 342 | } 343 | 344 | }else{ 345 | if cropMode == 1{ 346 | //16:9 347 | modeRatio = 16/9 348 | }else if cropMode == 2{ 349 | //4:3 350 | modeRatio = 4/3 351 | } 352 | if (cropHeight-value.translation.width/modeRatio > 40 || cropWidth-value.translation.width > 40) && 353 | //防止上边超过 354 | value.translation.width/modeRatio/2 + newPositionZ.height >= -imageDisplayHeight/2+cropHeight/2 && 355 | //防止下边超过————这里要注意有负号 356 | -value.translation.width/modeRatio/2 + newPositionZ.height <= imageDisplayHeight/2-cropHeight/2 && 357 | //防止右边超过 358 | currentPositionCrop.width+(cropWidth-value.translation.width)/2 <= imageDisplayWidth/2 && 359 | //防止左边超过 360 | currentPositionCrop.width-(cropWidth-value.translation.width)/2 >= -imageDisplayWidth/2 { 361 | currentPositionZ.width = value.translation.width + newPositionZ.width 362 | //相邻的角 363 | currentPositionZS.width = value.translation.width + newPositionZS.width 364 | currentPositionZS.height = value.translation.width/modeRatio/2 + newPositionZS.height 365 | currentPositionZX.width = value.translation.width + newPositionZX.width 366 | currentPositionZX.height = -value.translation.width/modeRatio/2 + newPositionZX.height 367 | //不相邻的角 368 | currentPositionYS.height = value.translation.width/modeRatio/2 + newPositionYS.height 369 | currentPositionYX.height = -value.translation.width/modeRatio/2 + newPositionYX.height 370 | //相邻的边 371 | currentPositionS.width = value.translation.width/2 + newPositionS.width 372 | currentPositionS.height = value.translation.width/modeRatio/2 + newPositionS.height 373 | currentPositionX.width = value.translation.width/2 + newPositionX.width 374 | currentPositionX.height = -value.translation.width/modeRatio/2 + newPositionX.height 375 | //裁剪器部分 376 | currentPositionCrop.width = value.translation.width/2 + newPositionCrop.width 377 | cropWidthAdd = -value.translation.width 378 | cropHeightAdd = -value.translation.width/modeRatio 379 | } 380 | } 381 | } 382 | .onEnded { value in 383 | operateOnEnd() 384 | } 385 | ) 386 | 387 | //Trailing 388 | Rectangle() 389 | .frame(width: 2, height: cropHeight + cropHeightAdd) 390 | .foregroundColor(cropBorderColor) 391 | .offset(x: currentPositionY.width + cropWidth/2, y: currentPositionY.height) 392 | .padding(.horizontal) 393 | .gesture( 394 | DragGesture() 395 | .onChanged { value in 396 | var modeRatio:CGFloat = 1.00 397 | if cropMode == 0{ 398 | if cropWidth+value.translation.width > 40 && value.translation.width + newPositionCrop.width <= imageDisplayWidth/2-cropWidth/2{ 399 | //自由模式 400 | currentPositionY.width = value.translation.width + newPositionY.width 401 | //相邻的角 402 | currentPositionYS.width = value.translation.width + newPositionYS.width 403 | currentPositionYS.width = value.translation.width + newPositionYS.width 404 | currentPositionYX.width = value.translation.width + newPositionYX.width 405 | currentPositionYX.width = value.translation.width + newPositionYX.width 406 | //相邻的边 407 | currentPositionS.width = value.translation.width/2 + newPositionS.width 408 | currentPositionX.width = value.translation.width/2 + newPositionX.width 409 | //裁剪器部分 410 | currentPositionCrop.width = value.translation.width/2 + newPositionCrop.width 411 | cropWidthAdd = value.translation.width 412 | } 413 | }else{ 414 | if cropMode == 1{ 415 | //16:9 416 | modeRatio = 16/9 417 | }else if cropMode == 2{ 418 | //4:3 419 | modeRatio = 4/3 420 | } 421 | if (cropHeight+value.translation.width/modeRatio > 40 || cropWidth+value.translation.width > 40) && 422 | //防止上边超过————这里要注意有负号 423 | -value.translation.width/modeRatio/2 + newPositionY.height >= -imageDisplayHeight/2+cropHeight/2 && 424 | //防止下边超过 425 | value.translation.width/modeRatio/2 + newPositionY.height <= imageDisplayHeight/2-cropHeight/2 && 426 | //防止右边超过 427 | currentPositionCrop.width+(cropWidth-value.translation.width)/2 <= imageDisplayWidth/2 && 428 | //防止左边超过 429 | currentPositionCrop.width-(cropWidth-value.translation.width)/2 >= -imageDisplayWidth/2 { 430 | currentPositionY.width = value.translation.width + newPositionY.width 431 | //相邻的角 432 | currentPositionYS.width = value.translation.width + newPositionYS.width 433 | currentPositionYS.height = -value.translation.width/modeRatio/2 + newPositionYS.height 434 | 435 | currentPositionYX.width = value.translation.width + newPositionYX.width 436 | currentPositionYX.height = value.translation.width/modeRatio/2 + newPositionYX.height 437 | //不相邻的角 438 | currentPositionZS.height = -value.translation.width/modeRatio/2 + newPositionZS.height 439 | currentPositionZX.height = value.translation.width/modeRatio/2 + newPositionZX.height 440 | //相邻的边 441 | currentPositionS.width = value.translation.width/2 + newPositionS.width 442 | currentPositionS.height = -value.translation.width/modeRatio/2 + newPositionS.height 443 | currentPositionX.width = value.translation.width/2 + newPositionX.width 444 | currentPositionX.height = value.translation.width/modeRatio/2 + newPositionX.height 445 | //裁剪器部分 446 | currentPositionCrop.width = value.translation.width/2 + newPositionCrop.width 447 | cropWidthAdd = value.translation.width 448 | cropHeightAdd = value.translation.width/modeRatio 449 | } 450 | } 451 | } 452 | .onEnded { value in 453 | operateOnEnd() 454 | } 455 | ) 456 | } 457 | } 458 | 459 | //Top-Leading 460 | Image(systemName: "arrow.up.left.and.down.right.and.arrow.up.right.and.down.left") 461 | .font(.system(size: 12)) 462 | .foregroundColor(cropVerticesColor) 463 | .background(Circle().frame(width: 20, height: 20).foregroundColor(Color.white)) 464 | .offset(x: currentPositionZS.width - cropWidth/2, y: currentPositionZS.height - cropHeight/2) 465 | .gesture( 466 | DragGesture() 467 | .onChanged { value in 468 | var modeRatio:CGFloat = 1 469 | if cropMode == 0{ 470 | //自由模式 471 | //水平方向 472 | if cropWidth-value.translation.width > 40 && value.translation.width+newPositionZS.width > -imageDisplayWidth/2+cropWidth/2 { 473 | currentPositionZS.width = value.translation.width + newPositionZS.width 474 | currentPositionZX.width = value.translation.width + newPositionZX.width 475 | //相邻的边 476 | currentPositionS.width = value.translation.width/2 + newPositionS.width 477 | currentPositionZ.width = value.translation.width + newPositionZ.width 478 | //不相邻的边 479 | currentPositionX.width = value.translation.width/2 + newPositionX.width 480 | //裁剪器部分 481 | currentPositionCrop.width = value.translation.width/2 + newPositionCrop.width 482 | cropWidthAdd = -value.translation.width 483 | } 484 | //垂直方向 485 | if cropHeight-value.translation.height > 40 && value.translation.height+newPositionZS.height > -imageDisplayHeight/2+cropHeight/2 { 486 | currentPositionZS.height = value.translation.height + newPositionZS.height 487 | currentPositionYS.height = value.translation.height + newPositionYS.height 488 | //相邻的边 489 | currentPositionS.height = value.translation.height + newPositionS.height 490 | currentPositionZ.height = value.translation.height/2 + newPositionZ.height 491 | //不相邻的边 492 | currentPositionY.height = value.translation.height/2 + newPositionY.height 493 | //裁剪器部分 494 | currentPositionCrop.height = value.translation.height/2 + newPositionCrop.height 495 | cropHeightAdd = -value.translation.height 496 | } 497 | }else{ 498 | if cropMode == 1{ 499 | //16:9 500 | modeRatio = 9/16 501 | }else if cropMode == 2{ 502 | //4:3 503 | modeRatio = 3/4 504 | } 505 | if (cropWidth-value.translation.width > 40 || cropHeight-value.translation.width*modeRatio > 40) && 506 | //防止上边超过————这里要注意有负号 507 | value.translation.height + newPositionZS.height >= -imageDisplayHeight/2+cropHeight/2 && 508 | //防止左边超过 509 | currentPositionCrop.width-(cropWidth-value.translation.width)/2 >= -imageDisplayWidth/2 { 510 | 511 | currentPositionZS.width = value.translation.width + newPositionZS.width 512 | currentPositionZS.height = value.translation.width*modeRatio + newPositionZS.height 513 | //相邻的角 514 | currentPositionZX.width = value.translation.width + newPositionZX.width 515 | currentPositionYS.height = value.translation.width*modeRatio + newPositionYS.height 516 | //相邻的边 517 | currentPositionS.width = value.translation.width/2 + newPositionS.width 518 | currentPositionS.height = value.translation.width*modeRatio + newPositionS.height 519 | currentPositionZ.width = value.translation.width + newPositionZ.width 520 | currentPositionZ.height = value.translation.width/2*modeRatio + newPositionZ.height 521 | //不相邻的边 522 | currentPositionX.width = value.translation.width/2 + newPositionX.width 523 | currentPositionY.height = value.translation.width/2*modeRatio + newPositionY.height 524 | //裁剪器部分 525 | currentPositionCrop.width = value.translation.width/2 + newPositionCrop.width 526 | currentPositionCrop.height = value.translation.width/2*modeRatio + newPositionCrop.height 527 | cropWidthAdd = -value.translation.width 528 | cropHeightAdd = -value.translation.width*modeRatio 529 | } 530 | } 531 | } 532 | .onEnded { value in 533 | operateOnEnd() 534 | } 535 | ) 536 | 537 | //Bottom-Leading 538 | Image(systemName: "arrow.up.left.and.down.right.and.arrow.up.right.and.down.left") 539 | .font(.system(size: 12)) 540 | .foregroundColor(cropVerticesColor) 541 | .background(Circle().frame(width: 20, height: 20).foregroundColor(Color.white)) 542 | .offset(x: currentPositionZX.width - cropWidth/2, y: currentPositionZX.height + cropHeight/2) 543 | .gesture( 544 | DragGesture() 545 | .onChanged { value in 546 | var modeRatio:CGFloat = 1 547 | 548 | if cropMode == 0{ 549 | if cropWidth-value.translation.width > 40 && value.translation.width+newPositionZX.width > -imageDisplayWidth/2+cropWidth/2{ 550 | currentPositionZX.width = value.translation.width + newPositionZX.width 551 | currentPositionZS.width = value.translation.width + newPositionZS.width 552 | //相邻的边 553 | currentPositionZ.width = value.translation.width + newPositionZ.width 554 | currentPositionX.width = value.translation.width/2 + newPositionX.width 555 | //不相邻的边 556 | currentPositionS.width = value.translation.width/2 + newPositionX.width 557 | 558 | currentPositionCrop.width = value.translation.width/2 + newPositionCrop.width 559 | cropWidthAdd = -value.translation.width 560 | } 561 | 562 | if cropHeight+value.translation.height > 40 && value.translation.height+newPositionZX.height < imageDisplayHeight/2-cropHeight/2 { 563 | currentPositionZX.height = value.translation.height + newPositionZX.height 564 | 565 | currentPositionYX.height = value.translation.height + newPositionYX.height 566 | 567 | currentPositionZ.height = value.translation.height/2 + newPositionZ.height 568 | currentPositionX.height = value.translation.height + newPositionX.height 569 | 570 | currentPositionY.height = value.translation.height/2 + newPositionY.height 571 | 572 | currentPositionCrop.height = value.translation.height/2 + newPositionCrop.height 573 | cropHeightAdd = value.translation.height 574 | } 575 | }else{ 576 | if cropMode == 1{ 577 | //16:9 578 | modeRatio = 9/16 579 | }else if cropMode == 2{ 580 | //4:3 581 | modeRatio = 3/4 582 | } 583 | if (cropWidth-value.translation.width > 40 || cropHeight-value.translation.width*modeRatio > 40) && 584 | //防止下边超过 585 | -value.translation.width*modeRatio + newPositionZX.height <= imageDisplayHeight/2-cropHeight/2 && 586 | //防止左边超过 587 | currentPositionCrop.width-(cropWidth-value.translation.width)/2 >= -imageDisplayWidth/2 { 588 | 589 | currentPositionZX.width = value.translation.width + newPositionZX.width 590 | currentPositionZX.height = -value.translation.width*modeRatio + newPositionZX.height 591 | 592 | currentPositionZS.width = value.translation.width + newPositionZS.width 593 | currentPositionYX.height = -value.translation.width*modeRatio + newPositionYX.height 594 | //相邻的边 595 | currentPositionZ.width = value.translation.width + newPositionZ.width 596 | currentPositionZ.height = -value.translation.width/2*modeRatio + newPositionZ.height 597 | 598 | currentPositionX.width = value.translation.width/2 + newPositionX.width 599 | currentPositionX.height = -value.translation.width*modeRatio + newPositionX.height 600 | 601 | //不相邻的边 602 | currentPositionY.height = -value.translation.width/2*modeRatio + newPositionY.height 603 | currentPositionS.width = value.translation.width/2 + newPositionX.width 604 | 605 | currentPositionCrop.width = value.translation.width/2 + newPositionCrop.width 606 | currentPositionCrop.height = -value.translation.width/2*modeRatio + newPositionCrop.height 607 | cropWidthAdd = -value.translation.width 608 | cropHeightAdd = -value.translation.width*modeRatio 609 | } 610 | } 611 | } 612 | .onEnded { value in 613 | operateOnEnd() 614 | } 615 | ) 616 | 617 | //Bottom-Trailing 618 | Image(systemName: "arrow.up.left.and.down.right.and.arrow.up.right.and.down.left") 619 | .font(.system(size: 12)) 620 | .foregroundColor(cropVerticesColor) 621 | .background(Circle().frame(width: 20, height: 20).foregroundColor(Color.white)) 622 | .offset(x: currentPositionYX.width + cropWidth/2, y: currentPositionYX.height + cropHeight/2) 623 | .gesture( 624 | DragGesture() 625 | .onChanged { value in 626 | var modeRatio:CGFloat = 1 627 | 628 | if cropMode == 0{ 629 | if cropWidth+value.translation.width > 40 && value.translation.width+newPositionYX.width < imageDisplayWidth/2-cropWidth/2{ 630 | currentPositionYX.width = value.translation.width + newPositionYX.width 631 | currentPositionYS.width = value.translation.width + newPositionYS.width 632 | //相邻的边 633 | currentPositionX.width = value.translation.width/2 + newPositionX.width 634 | currentPositionY.width = value.translation.width + newPositionY.width 635 | //不相邻的边 636 | currentPositionS.width = value.translation.width/2 + newPositionX.width 637 | 638 | currentPositionCrop.width = value.translation.width/2 + newPositionCrop.width 639 | cropWidthAdd = value.translation.width 640 | } 641 | 642 | if cropHeight+value.translation.height > 40 && value.translation.height+newPositionYX.height < imageDisplayHeight/2-cropHeight/2{ 643 | currentPositionYX.height = value.translation.height + newPositionYX.height 644 | currentPositionZX.height = value.translation.height + newPositionZX.height 645 | 646 | currentPositionX.height = value.translation.height + newPositionX.height 647 | currentPositionY.height = value.translation.height/2 + newPositionY.height 648 | 649 | currentPositionZ.height = value.translation.height/2 + newPositionZ.height 650 | 651 | currentPositionCrop.height = value.translation.height/2 + newPositionCrop.height 652 | cropHeightAdd = value.translation.height 653 | } 654 | }else{ 655 | if cropMode == 1{ 656 | //16:9 657 | modeRatio = 9/16 658 | }else if cropMode == 2{ 659 | //4:3 660 | modeRatio = 3/4 661 | } 662 | if (cropWidth+value.translation.width > 40 || cropHeight+value.translation.width*modeRatio > 40) && 663 | //防止下边超过 664 | value.translation.width*modeRatio + newPositionYX.height <= imageDisplayHeight/2-cropHeight/2 && 665 | //防止右边超过 666 | currentPositionCrop.width+(cropWidth+value.translation.width)/2 <= imageDisplayWidth/2 { 667 | 668 | currentPositionYX.width = value.translation.width + newPositionYX.width 669 | currentPositionYX.height = value.translation.width*modeRatio + newPositionYX.height 670 | 671 | currentPositionYS.width = value.translation.width + newPositionYS.width 672 | currentPositionZX.height = value.translation.width*modeRatio + newPositionZX.height 673 | 674 | //相邻的边 675 | currentPositionX.width = value.translation.width/2 + newPositionX.width 676 | currentPositionX.height = value.translation.width*modeRatio + newPositionX.height 677 | 678 | currentPositionY.width = value.translation.width + newPositionY.width 679 | currentPositionY.height = value.translation.width/2*modeRatio + newPositionY.height 680 | 681 | //不相邻的边 682 | currentPositionS.width = value.translation.width/2 + newPositionS.width 683 | currentPositionZ.height = value.translation.width/2*modeRatio + newPositionZ.height 684 | 685 | currentPositionCrop.width = value.translation.width/2 + newPositionCrop.width 686 | currentPositionCrop.height = value.translation.width/2*modeRatio + newPositionCrop.height 687 | cropWidthAdd = value.translation.width 688 | cropHeightAdd = value.translation.width*modeRatio 689 | } 690 | } 691 | } 692 | .onEnded { value in 693 | operateOnEnd() 694 | } 695 | ) 696 | 697 | //Bottom-Topping 698 | Image(systemName: "arrow.up.left.and.down.right.and.arrow.up.right.and.down.left") 699 | .font(.system(size: 12)) 700 | .foregroundColor(cropVerticesColor) 701 | .background(Circle().frame(width: 20, height: 20).foregroundColor(Color.white)) 702 | .offset(x: currentPositionYS.width + cropWidth/2, y: currentPositionYS.height - cropHeight/2) 703 | .gesture( 704 | DragGesture() 705 | .onChanged { value in 706 | var modeRatio:CGFloat = 1 707 | 708 | if cropMode == 0{ 709 | if cropWidth+value.translation.width > 40 && value.translation.width+newPositionYS.width < imageDisplayWidth/2-cropWidth/2{ 710 | currentPositionYS.width = value.translation.width + newPositionYS.width 711 | currentPositionYX.width = value.translation.width + newPositionYX.width 712 | //相邻的边 713 | currentPositionY.width = value.translation.width + newPositionY.width 714 | currentPositionS.width = value.translation.width/2 + newPositionX.width 715 | //不相邻的边 716 | currentPositionX.width = value.translation.width/2 + newPositionX.width 717 | 718 | currentPositionCrop.width = value.translation.width/2 + newPositionCrop.width 719 | cropWidthAdd = value.translation.width 720 | } 721 | 722 | if cropHeight-value.translation.height > 40 && -value.translation.height+newPositionYS.height < imageDisplayHeight/2-cropHeight/2{ 723 | currentPositionYS.height = value.translation.height + newPositionYS.height 724 | currentPositionZS.height = value.translation.height + newPositionZS.height 725 | 726 | currentPositionY.height = value.translation.height/2 + newPositionY.height 727 | currentPositionS.height = value.translation.height + newPositionX.height 728 | 729 | currentPositionZ.height = value.translation.height/2 + newPositionZ.height 730 | 731 | currentPositionCrop.height = value.translation.height/2 + newPositionCrop.height 732 | cropHeightAdd = -value.translation.height 733 | } 734 | }else{ 735 | if cropMode == 1{ 736 | //16:9 737 | modeRatio = 9/16 738 | }else if cropMode == 2{ 739 | //4:3 740 | modeRatio = 3/4 741 | } 742 | if (cropWidth+value.translation.width > 40 || cropHeight+value.translation.width*modeRatio > 40) && 743 | //防止上边超过 744 | -value.translation.width*modeRatio + newPositionZS.height >= -imageDisplayHeight/2+cropHeight/2 && 745 | //防止右边超过 746 | currentPositionCrop.width+(cropWidth+value.translation.width)/2 <= imageDisplayWidth/2 { 747 | 748 | currentPositionYS.width = value.translation.width + newPositionYS.width 749 | currentPositionYS.height = -value.translation.width*modeRatio + newPositionYS.height 750 | 751 | currentPositionYX.width = value.translation.width + newPositionYX.width 752 | currentPositionZS.height = -value.translation.width*modeRatio + newPositionZS.height 753 | 754 | //相邻的边 755 | currentPositionY.width = value.translation.width + newPositionY.width 756 | currentPositionY.height = -value.translation.width/2*modeRatio + newPositionY.height 757 | 758 | currentPositionS.width = value.translation.width/2 + newPositionX.width 759 | currentPositionS.height = -value.translation.width*modeRatio + newPositionX.height 760 | 761 | //不相邻的边 762 | currentPositionX.width = value.translation.width/2 + newPositionX.width 763 | currentPositionZ.height = -value.translation.width/2*modeRatio + newPositionZ.height 764 | 765 | currentPositionCrop.width = value.translation.width/2 + newPositionCrop.width 766 | currentPositionCrop.height = -value.translation.width/2*modeRatio + newPositionCrop.height 767 | cropWidthAdd = value.translation.width 768 | cropHeightAdd = value.translation.width*modeRatio 769 | } 770 | } 771 | } 772 | .onEnded { value in 773 | operateOnEnd() 774 | } 775 | ) 776 | } 777 | 778 | Spacer() 779 | 780 | HStack { 781 | HStack { 782 | Button (action : { 783 | cropMode = 0 784 | }) { 785 | if cropMode == 0 { 786 | Text("Custom") 787 | .padding(.all, 10) 788 | .foregroundColor(.white) 789 | .background(.gray) 790 | 791 | } else { 792 | Text("Custom") 793 | .foregroundColor(.white) 794 | .padding(.all, 10) 795 | } 796 | } 797 | 798 | Button (action : { 799 | cropMode = 1 800 | cropHeight = cropWidth*9/16 801 | if cropWidth > cropHeight{ 802 | cropWidth = cropHeight*16/9 803 | }else{ 804 | cropHeight = cropWidth*9/16 805 | } 806 | 807 | if currentPositionCrop.width >= imageDisplayWidth/2 - cropWidth/2{ 808 | currentPositionCrop.width = imageDisplayWidth/2 - cropWidth/2 809 | operateOnEnd() 810 | }else if currentPositionCrop.width <= -imageDisplayWidth/2 + cropWidth/2{ 811 | currentPositionCrop.width = -imageDisplayWidth/2 + cropWidth/2 812 | operateOnEnd() 813 | } 814 | }) { 815 | if cropMode == 1 { 816 | Text("16:9") 817 | .padding(.all, 10) 818 | .foregroundColor(.white) 819 | .background(.gray) 820 | } else { 821 | Text("16:9") 822 | .padding(.all, 10) 823 | .foregroundColor(.white) 824 | } 825 | } 826 | 827 | Button (action : { 828 | cropMode = 2 829 | if cropWidth > cropHeight{ 830 | cropWidth = cropHeight*4/3 831 | }else{ 832 | cropHeight = cropWidth*3/4 833 | } 834 | 835 | if currentPositionCrop.width >= imageDisplayWidth/2 - cropWidth/2{ 836 | currentPositionCrop.width = imageDisplayWidth/2 - cropWidth/2 837 | operateOnEnd() 838 | }else if currentPositionCrop.width <= -imageDisplayWidth/2 + cropWidth/2{ 839 | currentPositionCrop.width = -imageDisplayWidth/2 + cropWidth/2 840 | operateOnEnd() 841 | } 842 | }) { 843 | if cropMode == 2 { 844 | Text("4:3") 845 | .padding(.all, 10) 846 | .foregroundColor(.white) 847 | .background(.gray) 848 | } else { 849 | Text("4:3") 850 | .padding(.all, 10) 851 | .foregroundColor(.white) 852 | } 853 | } 854 | } 855 | .background(Color.gray.opacity(0.2)) 856 | .padding() 857 | 858 | Spacer() 859 | 860 | Button(action: {crop()}, label: { 861 | Image(systemName: "crop") 862 | .padding(.all, 10) 863 | .foregroundColor(.white) 864 | .background(Color.gray.opacity(0.2)) 865 | }) 866 | .padding() 867 | } 868 | } 869 | } 870 | .navigationBarHidden(true) 871 | .onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in 872 | 873 | if UIDevice.current.orientation.isPortrait { 874 | screenWidth = UIScreen.main.bounds.width 875 | screenHeight = UIScreen.main.bounds.height 876 | } else if UIDevice.current.orientation.isLandscape { 877 | screenWidth = UIScreen.main.bounds.height 878 | screenHeight = UIScreen.main.bounds.width 879 | } 880 | 881 | print("screenWidth: \(screenWidth), screenHeight: \(screenHeight)") 882 | } 883 | } 884 | 885 | func crop() { 886 | //由于CGRect是先到坐标再开始生成的,所以要这样减去剪裁栏的部分 887 | let rect = CGRect(x: imageDisplayWidth/2 + currentPositionCrop.width - cropWidth/2, 888 | y: imageDisplayHeight/2 + currentPositionCrop.height - cropHeight/2, 889 | width: cropWidth, 890 | height: cropHeight) 891 | croppedImage = cropImage(inputImage, toRect: rect, viewWidth: imageDisplayWidth, viewHeight: imageDisplayHeight)! 892 | self.presentationMode.wrappedValue.dismiss() 893 | } 894 | 895 | func operateOnEnd() { 896 | cropWidth = cropWidth + cropWidthAdd 897 | cropHeight = cropHeight + cropHeightAdd 898 | cropWidthAdd = 0 899 | cropHeightAdd = 0 900 | 901 | //Conners 902 | currentPositionZS.width = currentPositionCrop.width 903 | currentPositionZS.height = currentPositionCrop.height 904 | 905 | currentPositionZX.width = currentPositionCrop.width 906 | currentPositionZX.height = currentPositionCrop.height 907 | 908 | currentPositionYX.width = currentPositionCrop.width 909 | currentPositionYX.height = currentPositionCrop.height 910 | 911 | currentPositionYS.width = currentPositionCrop.width 912 | currentPositionYS.height = currentPositionCrop.height 913 | 914 | //Sides 915 | currentPositionS.width = currentPositionCrop.width 916 | currentPositionS.height = currentPositionCrop.height 917 | 918 | currentPositionZ.width = currentPositionCrop.width 919 | currentPositionZ.height = currentPositionCrop.height 920 | 921 | currentPositionX.width = currentPositionCrop.width 922 | currentPositionX.height = currentPositionCrop.height 923 | 924 | currentPositionY.width = currentPositionCrop.width 925 | currentPositionY.height = currentPositionCrop.height 926 | 927 | self.newPositionCrop = self.currentPositionCrop 928 | self.newPositionZS = self.currentPositionZS 929 | self.newPositionZX = self.currentPositionZX 930 | self.newPositionYX = self.currentPositionYX 931 | self.newPositionYS = self.currentPositionYS 932 | self.newPositionS = self.currentPositionS 933 | self.newPositionZ = self.currentPositionZ 934 | self.newPositionX = self.currentPositionX 935 | self.newPositionY = self.currentPositionY 936 | } 937 | } 938 | 939 | -------------------------------------------------------------------------------- /ImageCropper/ImageCropper/ImageCropperApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageCropperApp.swift 3 | // ImageCropper 4 | // 5 | // Created by 钟宜江 on 2022/1/20. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct ImageCropperApp: App { 12 | var body: some Scene { 13 | WindowGroup { 14 | ContentView() 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /ImageCropper/ImageCropper/NaviBarView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NaviBarView.swift 3 | // ImageCropper 4 | // 5 | // Created by 钟宜江 on 2023/8/5. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct NaviBarView: View { 11 | var body: some View { 12 | Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) 13 | } 14 | } 15 | 16 | struct NaviBarView_Previews: PreviewProvider { 17 | static var previews: some View { 18 | NaviBarView() 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 ZhongUncle 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SwiftUI_ImageCropper 2 | ## Introduction 3 | This is a simple cropper of images using SwiftUI. So you can customize the cropper easily and crop image in iOS/iPadOS even in macOS. 4 | 5 | I make a demo in iOS/iPadOS to help you know how it works, and it also shows the size of image in pixel. 6 | It will show an image I inputted in advance. Then you can crop it in your mind, and touch the symbol "crop". The cropped image will show! 7 | It works like below in iPhone and iPad: 8 | 9 | iPhone: 10 | 11 | 12 | 13 | https://user-images.githubusercontent.com/78771985/203647476-c56e0f02-f3d9-44bf-9deb-d246beab2e93.mp4 14 | 15 | 16 | 17 | iPad: 18 | 19 | 20 | 21 | https://user-images.githubusercontent.com/78771985/203647458-cb9dd24e-d026-433d-999d-b2a89eb46a0a.mov 22 | 23 | 24 | 25 | 26 | You can follow the Usage to take SwiftUI_ImageCropper into your app or project! Enjoy it! 27 | 28 | ## Usage 29 | ### Step 1 30 | Copy or drag "CropperView" and "CropImage" to your project. (The package is developping, coming soon) 31 | 32 | ![Files' name](https://user-images.githubusercontent.com/78771985/178088277-ff7e71a1-dd88-4a14-b387-818b0712f59f.jpeg) 33 | 34 | ### Step 2 35 | Then you can use following code to call it in any View: 36 | ``` 37 | CropperView(inputImage: UIImage, croppedImage: Binding, cropBorderColor: Color?, cropVerticesColor: Color, cropperOutsideOpacity: Double) 38 | ``` 39 | 40 | There are some parameters for you to customize or use the cropper(**If you want more, please "issue" me**): 41 | 42 | | Parameter | Type | Default | Description | 43 | | ----------- | ----------- | ----------- | ----------- | 44 | | inputImage | UIImage | You must assign it | Image you want to be cropped | 45 | | croppedImage | Binding | You must assign it | The cropped image(it need you create a UIImage first), you can show or save it | 46 | | cropBorderColor | Color? | Color.white | As name said, it control the color of border of cropper | 47 | | cropVerticesColor | Color | Color.pink | As name said, it control the color of Vertices of cropper | 48 | | cropperOutsideOpacity | Double | 0.4 | The opacity of dark mask out of cropper | 49 | 50 | There are some demoes to show the changes in order (Defult vs Changed). 51 | 52 | *cropBorderColor: Defult vs Color.yellow* 53 | ![cropBorderColor](https://user-images.githubusercontent.com/78771985/178088422-c64ef29c-05d9-439f-bc4a-33e1f322a0ab.jpg) 54 | 55 | *cropVerticesColor: Defult vs Color.yellow* 56 | ![cropVerticesColor](https://user-images.githubusercontent.com/78771985/178088425-4b0f76b0-0b5a-466d-b4c8-a863259e80a6.jpg) 57 | 58 | *cropperOutsideOpacity: Defult vs 0.8* 59 | ![cropperOutsideOpacity](https://user-images.githubusercontent.com/78771985/178088742-788303ae-bc66-480e-95e2-0ca31c280152.jpg) 60 | 61 | 62 | ## Notices 63 | 64 | If you check the source code, you will find some issues "Invalid frame dimension (negative or non-finite)." 65 | Rest assured, they will not affect the operation. 66 | 67 | ## Bugs 68 | If you find a bug, please report it! --------------------------------------------------------------------------------