├── PhotoTweaks ├── Images.xcassets │ └── AppIcon.appiconset │ │ ├── PhotoTweak.png │ │ └── Contents.json ├── en.lproj │ └── PhotoTweaks.strings ├── zh-Hans.lproj │ └── PhotoTweaks.strings ├── AppDelegate.h ├── main.m ├── PhotoTweaks │ ├── UIColor+Tweak.h │ ├── PhotoTweakView.h │ ├── UIColor+Tweak.m │ ├── PhotoTweaksViewController.h │ ├── PhotoTweaksViewController.m │ └── PhotoTweakView.m ├── Base.lproj │ ├── Main.storyboard │ └── LaunchScreen.xib ├── Info.plist └── AppDelegate.m ├── .gitignore ├── PhotoTweaks.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcuserdata │ │ ├── tuyou.xcuserdatad │ │ │ └── UserInterfaceState.xcuserstate │ │ └── Chandler.xcuserdatad │ │ │ └── WorkspaceSettings.xcsettings │ └── xcshareddata │ │ └── PhotoTweaks.xccheckout ├── xcshareddata │ └── xcschemes │ │ └── PhotoTweaks.xcscheme └── project.pbxproj ├── .travis.yml ├── PhotoTweaks.podspec ├── PhotoTweaksTests ├── Info.plist └── PhotoTweaksTests.m ├── LICENSE └── README.md /PhotoTweaks/Images.xcassets/AppIcon.appiconset/PhotoTweak.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itouch2/PhotoTweaks/HEAD/PhotoTweaks/Images.xcassets/AppIcon.appiconset/PhotoTweak.png -------------------------------------------------------------------------------- /PhotoTweaks/en.lproj/PhotoTweaks.strings: -------------------------------------------------------------------------------- 1 | /* 2 | PhotoTweaks.strings 3 | PhotoTweaks 4 | 5 | Created by BruceZCQ on 3/31/15. 6 | Copyright (c) 2015 Tu You. All rights reserved. 7 | */ 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | *.xcbkptlist 3 | 4 | *.xcuserstate 5 | 6 | *.xcuserstate 7 | 8 | *.xcuserstate 9 | 10 | PhotoTweaks.xcodeproj/xcuserdata/Chandler.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist 11 | -------------------------------------------------------------------------------- /PhotoTweaks.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /PhotoTweaks.xcodeproj/project.xcworkspace/xcuserdata/tuyou.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itouch2/PhotoTweaks/HEAD/PhotoTweaks.xcodeproj/project.xcworkspace/xcuserdata/tuyou.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /PhotoTweaks/zh-Hans.lproj/PhotoTweaks.strings: -------------------------------------------------------------------------------- 1 | /* 2 | PhotoTweaks.strings 3 | PhotoTweaks 4 | 5 | Created by BruceZCQ on 3/31/15. 6 | Copyright (c) 2015 Tu You. All rights reserved. 7 | */ 8 | 9 | "Cancel"="取消"; 10 | "Done"="完成"; 11 | "RESET"="重置"; -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | xcode_project: PhotoTweaks.xcodeproj 3 | xcode_scheme: PhotoTweaks 4 | script: xcodebuild build -sdk iphonesimulator11 -project PhotoTweaks.xcodeproj -scheme PhotoTweaks CODE_SIGNING_REQUIRED=NO 5 | xcode_sdk: 6 | - iphonesimulator11 7 | -------------------------------------------------------------------------------- /PhotoTweaks/AppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.h 3 | // PhotoTweaks 4 | // 5 | // Created by Tu You on 14/11/28. 6 | // Copyright (c) 2014年 Tu You. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface AppDelegate : UIResponder 12 | 13 | @property (strong, nonatomic) UIWindow *window; 14 | 15 | 16 | @end 17 | 18 | -------------------------------------------------------------------------------- /PhotoTweaks/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // PhotoTweaks 4 | // 5 | // Created by Tu You on 14/11/28. 6 | // Copyright (c) 2014年 Tu You. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "AppDelegate.h" 11 | 12 | int main(int argc, char * argv[]) { 13 | @autoreleasepool { 14 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /PhotoTweaks.xcodeproj/project.xcworkspace/xcuserdata/Chandler.xcuserdatad/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | HasAskedToTakeAutomaticSnapshotBeforeSignificantChanges 6 | 7 | SnapshotAutomaticallyBeforeSignificantChanges 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /PhotoTweaks/PhotoTweaks/UIColor+Tweak.h: -------------------------------------------------------------------------------- 1 | // 2 | // UIColor+Tweak.h 3 | // PhotoTweaks 4 | // 5 | // Created by TuYou on 14/12/6. 6 | // Copyright (c) 2014年 Tu You. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface UIColor (Tweak) 12 | 13 | + (UIColor *)cancelButtonColor; 14 | + (UIColor *)cancelButtonHighlightedColor; 15 | 16 | + (UIColor *)saveButtonColor; 17 | + (UIColor *)saveButtonHighlightedColor; 18 | 19 | + (UIColor *)resetButtonColor; 20 | + (UIColor *)resetButtonHighlightedColor; 21 | 22 | + (UIColor *)cropLineColor; 23 | + (UIColor *)gridLineColor; 24 | + (UIColor *)maskColor; 25 | + (UIColor *)photoTweakCanvasBackgroundColor; 26 | 27 | @end 28 | -------------------------------------------------------------------------------- /PhotoTweaks.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |spec| 2 | spec.name = 'PhotoTweaks' 3 | spec.version = '1.0.4' 4 | spec.license = 'MIT' 5 | spec.homepage = 'https://github.com/itouch2/PhotoTweaks' 6 | spec.authors = {'Tu You' => 'yoututouch@gmail.com'} 7 | spec.summary = 'Drag, Rotate, Scale and Crop.' 8 | spec.source = {:git => 'https://github.com/itouch2/PhotoTweaks.git', :tag => '1.0.4'} 9 | spec.source_files = 'PhotoTweaks/PhotoTweaks/*.{h,m}' 10 | spec.framework = 'Foundation', 'CoreGraphics', 'UIKit' 11 | spec.requires_arc = true 12 | spec.platform = :ios, '7.0' 13 | spec.screenshot = 'https://cloud.githubusercontent.com/assets/4316898/6525485/ce2d65ae-c440-11e4-8a73-c461a3f31b5f.png' 14 | end 15 | -------------------------------------------------------------------------------- /PhotoTweaksTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | tuyou.$(PRODUCT_NAME:rfc1034identifier) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /PhotoTweaksTests/PhotoTweaksTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // PhotoTweaksTests.m 3 | // PhotoTweaksTests 4 | // 5 | // Created by Tu You on 14/11/28. 6 | // Copyright (c) 2014年 Tu You. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | 12 | @interface PhotoTweaksTests : XCTestCase 13 | 14 | @end 15 | 16 | @implementation PhotoTweaksTests 17 | 18 | - (void)setUp { 19 | [super setUp]; 20 | // Put setup code here. This method is called before the invocation of each test method in the class. 21 | } 22 | 23 | - (void)tearDown { 24 | // Put teardown code here. This method is called after the invocation of each test method in the class. 25 | [super tearDown]; 26 | } 27 | 28 | - (void)testExample { 29 | // This is an example of a functional test case. 30 | XCTAssert(YES, @"Pass"); 31 | } 32 | 33 | - (void)testPerformanceExample { 34 | // This is an example of a performance test case. 35 | [self measureBlock:^{ 36 | // Put the code you want to measure the time of here. 37 | }]; 38 | } 39 | 40 | @end 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 tuyou 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 | 23 | -------------------------------------------------------------------------------- /PhotoTweaks/PhotoTweaks/PhotoTweakView.h: -------------------------------------------------------------------------------- 1 | // 2 | // PhotoView.h 3 | // PhotoTweaks 4 | // 5 | // Created by Tu You on 14/12/2. 6 | // Copyright (c) 2014年 Tu You. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | extern const CGFloat kMaxRotationAngle; 12 | 13 | @class CropView; 14 | 15 | @interface PhotoContentView : UIView 16 | 17 | @property (strong, nonatomic) UIImageView *imageView; 18 | @property (strong, nonatomic) UIImage *image; 19 | 20 | @end 21 | 22 | @protocol CropViewDelegate 23 | 24 | - (void)cropEnded:(CropView *)cropView; 25 | - (void)cropMoved:(CropView *)cropView; 26 | 27 | @end 28 | 29 | @interface CropView : UIView 30 | @end 31 | 32 | @interface PhotoTweakView : UIView 33 | 34 | @property (nonatomic, assign, readonly) CGFloat angle; 35 | @property (nonatomic, assign, readonly) CGPoint photoContentOffset; 36 | 37 | @property (nonatomic, strong, readonly) CropView *cropView; 38 | @property (nonatomic, strong, readonly) PhotoContentView *photoContentView; 39 | @property (nonatomic, strong, readonly) UISlider *slider; 40 | @property (nonatomic, strong, readonly) UIButton *resetBtn; 41 | 42 | 43 | - (instancetype)initWithFrame:(CGRect)frame 44 | image:(UIImage *)image 45 | maxRotationAngle:(CGFloat)maxRotationAngle; 46 | 47 | - (instancetype)initWithFrame:(CGRect)frame 48 | image:(UIImage *)image; 49 | 50 | - (CGPoint)photoTranslation; 51 | 52 | @end 53 | -------------------------------------------------------------------------------- /PhotoTweaks/PhotoTweaks/UIColor+Tweak.m: -------------------------------------------------------------------------------- 1 | // 2 | // UIColor+Tweak.m 3 | // PhotoTweaks 4 | // 5 | // Created by TuYou on 14/12/6. 6 | // Copyright (c) 2014年 Tu You. All rights reserved. 7 | // 8 | 9 | #import "UIColor+Tweak.h" 10 | 11 | @implementation UIColor (Tweak) 12 | 13 | + (UIColor *)cancelButtonColor 14 | { 15 | return [UIColor colorWithRed:0.09 green:0.49 blue:1 alpha:1]; 16 | } 17 | 18 | + (UIColor *)cancelButtonHighlightedColor 19 | { 20 | return [UIColor colorWithRed:0.11 green:0.17 blue:0.26 alpha:1]; 21 | } 22 | 23 | + (UIColor *)saveButtonColor 24 | { 25 | return [UIColor colorWithRed:1 green:0.8 blue:0 alpha:1]; 26 | } 27 | 28 | + (UIColor *)saveButtonHighlightedColor 29 | { 30 | return [UIColor colorWithRed:0.26 green:0.23 blue:0.13 alpha:1]; 31 | } 32 | 33 | + (UIColor *)resetButtonColor 34 | { 35 | return [UIColor colorWithRed:0.09 green:0.49 blue:1 alpha:1]; 36 | } 37 | 38 | + (UIColor *)resetButtonHighlightedColor 39 | { 40 | return [UIColor colorWithRed:0.11 green:0.17 blue:0.26 alpha:1]; 41 | } 42 | 43 | + (UIColor *)maskColor 44 | { 45 | return [UIColor colorWithWhite:0.0 alpha:0.6]; 46 | } 47 | 48 | + (UIColor *)cropLineColor 49 | { 50 | return [UIColor colorWithWhite:1.0 alpha:1.0]; 51 | } 52 | 53 | + (UIColor *)gridLineColor 54 | { 55 | return [UIColor colorWithRed:0.52 green:0.48 blue:0.47 alpha:0.8]; 56 | } 57 | 58 | + (UIColor *)photoTweakCanvasBackgroundColor 59 | { 60 | return [UIColor colorWithWhite:0.0 alpha:1.0]; 61 | } 62 | 63 | @end 64 | -------------------------------------------------------------------------------- /PhotoTweaks/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 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /PhotoTweaks.xcodeproj/project.xcworkspace/xcshareddata/PhotoTweaks.xccheckout: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDESourceControlProjectFavoriteDictionaryKey 6 | 7 | IDESourceControlProjectIdentifier 8 | F6B6C3E5-ABF7-4307-97CD-5615215C3360 9 | IDESourceControlProjectName 10 | PhotoTweaks 11 | IDESourceControlProjectOriginsDictionary 12 | 13 | 69DED6FD9E7FD7F98E45C3020DA22B3A6748FD94 14 | https://github.com/itouch2/PhotoTweaks.git 15 | 16 | IDESourceControlProjectPath 17 | PhotoTweaks.xcodeproj 18 | IDESourceControlProjectRelativeInstallPathDictionary 19 | 20 | 69DED6FD9E7FD7F98E45C3020DA22B3A6748FD94 21 | ../.. 22 | 23 | IDESourceControlProjectURL 24 | https://github.com/itouch2/PhotoTweaks.git 25 | IDESourceControlProjectVersion 26 | 111 27 | IDESourceControlProjectWCCIdentifier 28 | 69DED6FD9E7FD7F98E45C3020DA22B3A6748FD94 29 | IDESourceControlProjectWCConfigurations 30 | 31 | 32 | IDESourceControlRepositoryExtensionIdentifierKey 33 | public.vcs.git 34 | IDESourceControlWCCIdentifierKey 35 | 69DED6FD9E7FD7F98E45C3020DA22B3A6748FD94 36 | IDESourceControlWCCName 37 | PhotoTweaks 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /PhotoTweaks/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | tuyou.$(PRODUCT_NAME:rfc1034identifier) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | UIInterfaceOrientationLandscapeRight 38 | 39 | UISupportedInterfaceOrientations~ipad 40 | 41 | UIInterfaceOrientationPortrait 42 | UIInterfaceOrientationPortraitUpsideDown 43 | UIInterfaceOrientationLandscapeLeft 44 | UIInterfaceOrientationLandscapeRight 45 | 46 | NSPhotoLibraryUsageDescription 47 | PhotoTweaks needs to use Photo Library 48 | NSPhotoLibraryAddUsageDescription 49 | PhotoTweaks needs to use Photo Library 50 | 51 | 52 | -------------------------------------------------------------------------------- /PhotoTweaks/Images.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 | "size" : "60x60", 35 | "idiom" : "iphone", 36 | "filename" : "PhotoTweak.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "idiom" : "iphone", 41 | "size" : "60x60", 42 | "scale" : "3x" 43 | }, 44 | { 45 | "idiom" : "ipad", 46 | "size" : "20x20", 47 | "scale" : "1x" 48 | }, 49 | { 50 | "idiom" : "ipad", 51 | "size" : "20x20", 52 | "scale" : "2x" 53 | }, 54 | { 55 | "idiom" : "ipad", 56 | "size" : "29x29", 57 | "scale" : "1x" 58 | }, 59 | { 60 | "idiom" : "ipad", 61 | "size" : "29x29", 62 | "scale" : "2x" 63 | }, 64 | { 65 | "idiom" : "ipad", 66 | "size" : "40x40", 67 | "scale" : "1x" 68 | }, 69 | { 70 | "idiom" : "ipad", 71 | "size" : "40x40", 72 | "scale" : "2x" 73 | }, 74 | { 75 | "idiom" : "ipad", 76 | "size" : "76x76", 77 | "scale" : "1x" 78 | }, 79 | { 80 | "idiom" : "ipad", 81 | "size" : "76x76", 82 | "scale" : "2x" 83 | }, 84 | { 85 | "idiom" : "ipad", 86 | "size" : "83.5x83.5", 87 | "scale" : "2x" 88 | }, 89 | { 90 | "idiom" : "ios-marketing", 91 | "size" : "1024x1024", 92 | "scale" : "1x" 93 | } 94 | ], 95 | "info" : { 96 | "version" : 1, 97 | "author" : "xcode" 98 | } 99 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 |

PhotoTweaks

5 | 6 | PhotoTweaks is an interface to crop photos. It can let user drag, rotate, scale the image, and crop it. You will find it mimics the interaction of Photos.app on iOS 8. :] 7 | 8 | [![Pod Version](http://img.shields.io/cocoapods/v/PhotoTweaks.svg?style=flat)](http://cocoapods.org/?q=PhotoTweaks) 9 | [![Platform](http://img.shields.io/cocoapods/p/PhotoTweaks.svg?style=flat)](http://cocoapods.org/?q=PhotoTweaks) 10 | [![License](http://img.shields.io/cocoapods/l/PhotoTweaks.svg?style=flat)](https://github.com/itouch2/PhotoTweaks/blob/master/LICENSE) 11 | 12 | ## Usage 13 | 14 | PhotoTweaksViewController offers all the operations to crop the photo, which includes translation, rotate and scale. 15 | 16 | To use it, 17 | 18 | ```objective-c 19 | UIImage *image = [info objectForKey:UIImagePickerControllerOriginalImage]; 20 | PhotoTweaksViewController *photoTweaksViewController = [[PhotoTweaksViewController alloc] initWithImage:image]; 21 | photoTweaksViewController.delegate = self; 22 | photoTweaksViewController.autoSaveToLibray = YES; 23 | photoTweaksViewController.maxRotationAngle = M_PI_4; 24 | [picker pushViewController:photoTweaksViewController animated:YES]; 25 | ``` 26 | 27 | ```maxRotationAngle``` is the property to set the maximum supported rotation angle. 28 | 29 | Get the cropped image 30 | ```objective-c 31 | - (void)photoTweaksController:(PhotoTweaksViewController *)controller didFinishWithCroppedImage:(UIImage *)croppedImage 32 | { 33 | [controller.navigationController dismissViewControllerAnimated:YES completion:nil]; 34 | // cropped image 35 | } 36 | ``` 37 | ## Installation 38 | PhotoTweaks is available on [CocoaPods](http://cocoapods.org). Add the follwing to your Podfile: 39 | ```ruby 40 | pod 'PhotoTweaks', '~> 1.0.4' 41 | ``` 42 | Alternatively, you can manually drag the ```PhotoTweaks``` folder into your Xcode project. 43 | 44 | 45 | ## A Quick Peek 46 | 47 | ![screenshots](https://cloud.githubusercontent.com/assets/4316898/6712965/84ab1d16-cdca-11e4-912a-f437bbb02d42.gif) 48 | 49 | ## Protip 50 | If using with an existing UIImagePickerController, be sure to set ```allowsEditing = NO``` otherwise you may force the user to crop with the native editing tool before showing PhotoTweaksViewController. 51 | -------------------------------------------------------------------------------- /PhotoTweaks/PhotoTweaks/PhotoTweaksViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // PhotoTweaksViewController.h 3 | // PhotoTweaks 4 | // 5 | // Created by Tu You on 14/12/5. 6 | // Copyright (c) 2014年 Tu You. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @protocol PhotoTweaksViewControllerDelegate; 12 | 13 | /** 14 | The photo tweaks controller. 15 | */ 16 | @interface PhotoTweaksViewController : UIViewController 17 | 18 | /** 19 | Image to process. 20 | */ 21 | @property (nonatomic, strong, readonly) UIImage *image; 22 | 23 | /** 24 | Flag indicating whether the image cropped will be saved to photo library automatically. Defaults to YES. 25 | */ 26 | @property (nonatomic, assign) BOOL autoSaveToLibray; 27 | 28 | /** 29 | Max rotation angle 30 | */ 31 | @property (nonatomic, assign) CGFloat maxRotationAngle; 32 | 33 | /** 34 | The optional photo tweaks controller delegate. 35 | */ 36 | @property (nonatomic, weak) id delegate; 37 | 38 | /** 39 | Save action button's default title color 40 | */ 41 | @property (nonatomic, strong) UIColor *saveButtonTitleColor; 42 | 43 | /** 44 | Save action button's highlight title color 45 | */ 46 | @property (nonatomic, strong) UIColor *saveButtonHighlightTitleColor; 47 | 48 | /** 49 | Cancel action button's default title color 50 | */ 51 | @property (nonatomic, strong) UIColor *cancelButtonTitleColor; 52 | 53 | /** 54 | Cancel action button's highlight title color 55 | */ 56 | @property (nonatomic, strong) UIColor *cancelButtonHighlightTitleColor; 57 | 58 | /** 59 | Reset action button's default title color 60 | */ 61 | @property (nonatomic, strong) UIColor *resetButtonTitleColor; 62 | 63 | /** 64 | Reset action button's highlight title color 65 | */ 66 | @property (nonatomic, strong) UIColor *resetButtonHighlightTitleColor; 67 | 68 | /** 69 | Slider tint color 70 | */ 71 | @property (nonatomic, strong) UIColor *sliderTintColor; 72 | 73 | /** 74 | Creates a photo tweaks view controller with the image to process. 75 | */ 76 | - (instancetype)initWithImage:(UIImage *)image; 77 | 78 | @end 79 | 80 | /** 81 | The photo tweaks controller delegate 82 | */ 83 | @protocol PhotoTweaksViewControllerDelegate 84 | 85 | /** 86 | Called on image cropped. 87 | */ 88 | - (void)photoTweaksController:(PhotoTweaksViewController *)controller didFinishWithCroppedImage:(UIImage *)croppedImage; 89 | 90 | /** 91 | Called on cropping image canceled 92 | */ 93 | - (void)photoTweaksControllerDidCancel:(PhotoTweaksViewController *)controller; 94 | 95 | @end 96 | -------------------------------------------------------------------------------- /PhotoTweaks/AppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.m 3 | // PhotoTweaks 4 | // 5 | // Created by Tu You on 14/11/28. 6 | // Copyright (c) 2014年 Tu You. All rights reserved. 7 | // 8 | 9 | #import "AppDelegate.h" 10 | #import "PhotoTweaksViewController.h" 11 | 12 | @interface AppDelegate () 13 | 14 | @end 15 | 16 | @implementation AppDelegate 17 | 18 | 19 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 20 | { 21 | // Override point for customization after application launch. 22 | 23 | UIImagePickerController *picker = [[UIImagePickerController alloc] init]; 24 | picker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary; 25 | picker.delegate = self; 26 | picker.allowsEditing = NO; 27 | picker.navigationBarHidden = YES; 28 | self.window.rootViewController = picker; 29 | 30 | return YES; 31 | } 32 | 33 | - (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info 34 | { 35 | UIImage *image = [info objectForKey:UIImagePickerControllerOriginalImage]; 36 | PhotoTweaksViewController *photoTweaksViewController = [[PhotoTweaksViewController alloc] initWithImage:image]; 37 | photoTweaksViewController.delegate = self; 38 | photoTweaksViewController.autoSaveToLibray = YES; 39 | photoTweaksViewController.maxRotationAngle = M_PI_4; 40 | [picker pushViewController:photoTweaksViewController animated:YES]; 41 | } 42 | 43 | - (void)photoTweaksController:(PhotoTweaksViewController *)controller didFinishWithCroppedImage:(UIImage *)croppedImage 44 | { 45 | [controller.navigationController popViewControllerAnimated:YES]; 46 | } 47 | 48 | - (void)photoTweaksControllerDidCancel:(PhotoTweaksViewController *)controller 49 | { 50 | [controller.navigationController popViewControllerAnimated:YES]; 51 | } 52 | 53 | - (void)applicationWillResignActive:(UIApplication *)application { 54 | // 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. 55 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 56 | } 57 | 58 | - (void)applicationDidEnterBackground:(UIApplication *)application { 59 | // 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. 60 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 61 | } 62 | 63 | - (void)applicationWillEnterForeground:(UIApplication *)application { 64 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 65 | } 66 | 67 | - (void)applicationDidBecomeActive:(UIApplication *)application { 68 | // 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. 69 | } 70 | 71 | - (void)applicationWillTerminate:(UIApplication *)application { 72 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 73 | } 74 | 75 | @end 76 | -------------------------------------------------------------------------------- /PhotoTweaks/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 20 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /PhotoTweaks.xcodeproj/xcshareddata/xcschemes/PhotoTweaks.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 47 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 65 | 66 | 75 | 76 | 82 | 83 | 84 | 85 | 86 | 87 | 93 | 94 | 100 | 101 | 102 | 103 | 105 | 106 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /PhotoTweaks/PhotoTweaks/PhotoTweaksViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // PhotoTweaksViewController.m 3 | // PhotoTweaks 4 | // 5 | // Created by Tu You on 14/12/5. 6 | // Copyright (c) 2014年 Tu You. All rights reserved. 7 | // 8 | 9 | #import "PhotoTweaksViewController.h" 10 | #import "PhotoTweakView.h" 11 | #import "UIColor+Tweak.h" 12 | #import 13 | 14 | @interface PhotoTweaksViewController () 15 | 16 | @property (strong, nonatomic) PhotoTweakView *photoView; 17 | 18 | @end 19 | 20 | @implementation PhotoTweaksViewController 21 | 22 | - (instancetype)initWithImage:(UIImage *)image 23 | { 24 | if (self = [super init]) { 25 | _image = image; 26 | _autoSaveToLibray = YES; 27 | _maxRotationAngle = kMaxRotationAngle; 28 | } 29 | return self; 30 | } 31 | 32 | - (void)viewDidLoad 33 | { 34 | [super viewDidLoad]; 35 | 36 | self.navigationController.navigationBarHidden = YES; 37 | 38 | if ([self respondsToSelector:@selector(automaticallyAdjustsScrollViewInsets)]) { 39 | self.automaticallyAdjustsScrollViewInsets = NO; 40 | } 41 | 42 | self.view.clipsToBounds = YES; 43 | self.view.backgroundColor = [UIColor photoTweakCanvasBackgroundColor]; 44 | 45 | [self setupSubviews]; 46 | } 47 | 48 | - (void)setupSubviews 49 | { 50 | self.photoView = [[PhotoTweakView alloc] initWithFrame:self.view.bounds image:self.image maxRotationAngle:self.maxRotationAngle]; 51 | self.photoView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; 52 | [self.view addSubview:self.photoView]; 53 | 54 | UIButton *cancelBtn = [UIButton buttonWithType:UIButtonTypeCustom]; 55 | cancelBtn.frame = CGRectMake(8, CGRectGetHeight(self.view.frame) - 40, 60, 40); 56 | cancelBtn.titleLabel.textAlignment = NSTextAlignmentLeft; 57 | [cancelBtn setTitle:NSLocalizedStringFromTable(@"Cancel", @"PhotoTweaks", nil) forState:UIControlStateNormal]; 58 | UIColor *cancelTitleColor = !self.cancelButtonTitleColor ? 59 | [UIColor cancelButtonColor] : self.cancelButtonTitleColor; 60 | [cancelBtn setTitleColor:cancelTitleColor forState:UIControlStateNormal]; 61 | UIColor *cancelHighlightTitleColor = !self.cancelButtonHighlightTitleColor ? 62 | [UIColor cancelButtonHighlightedColor] : self.cancelButtonHighlightTitleColor; 63 | [cancelBtn setTitleColor:cancelHighlightTitleColor forState:UIControlStateHighlighted]; 64 | cancelBtn.titleLabel.font = [UIFont systemFontOfSize:17]; 65 | [cancelBtn addTarget:self action:@selector(cancelBtnTapped) forControlEvents:UIControlEventTouchUpInside]; 66 | [self.view addSubview:cancelBtn]; 67 | 68 | UIButton *cropBtn = [UIButton buttonWithType:UIButtonTypeCustom]; 69 | cropBtn.titleLabel.textAlignment = NSTextAlignmentRight; 70 | cropBtn.frame = CGRectMake(CGRectGetWidth(self.view.frame) - 60, CGRectGetHeight(self.view.frame) - 40, 60, 40); 71 | [cropBtn setTitle:NSLocalizedStringFromTable(@"Done", @"PhotoTweaks", nil) forState:UIControlStateNormal]; 72 | UIColor *saveButtonTitleColor = !self.saveButtonTitleColor ? 73 | [UIColor saveButtonColor] : self.saveButtonTitleColor; 74 | [cropBtn setTitleColor:saveButtonTitleColor forState:UIControlStateNormal]; 75 | 76 | UIColor *saveButtonHighlightTitleColor = !self.saveButtonHighlightTitleColor ? 77 | [UIColor saveButtonHighlightedColor] : self.saveButtonHighlightTitleColor; 78 | [cropBtn setTitleColor:saveButtonHighlightTitleColor forState:UIControlStateHighlighted]; 79 | cropBtn.titleLabel.font = [UIFont systemFontOfSize:17]; 80 | [cropBtn addTarget:self action:@selector(saveBtnTapped) forControlEvents:UIControlEventTouchUpInside]; 81 | [self.view addSubview:cropBtn]; 82 | } 83 | 84 | - (void)cancelBtnTapped 85 | { 86 | [self.delegate photoTweaksControllerDidCancel:self]; 87 | } 88 | 89 | - (void)saveBtnTapped 90 | { 91 | CGAffineTransform transform = CGAffineTransformIdentity; 92 | 93 | // translate 94 | CGPoint translation = [self.photoView photoTranslation]; 95 | transform = CGAffineTransformTranslate(transform, translation.x, translation.y); 96 | 97 | // rotate 98 | transform = CGAffineTransformRotate(transform, self.photoView.angle); 99 | 100 | // scale 101 | CGAffineTransform t = self.photoView.photoContentView.transform; 102 | CGFloat xScale = sqrt(t.a * t.a + t.c * t.c); 103 | CGFloat yScale = sqrt(t.b * t.b + t.d * t.d); 104 | transform = CGAffineTransformScale(transform, xScale, yScale); 105 | 106 | CGImageRef imageRef = [self newTransformedImage:transform 107 | sourceImage:self.image.CGImage 108 | sourceSize:self.image.size 109 | sourceOrientation:self.image.imageOrientation 110 | outputWidth:self.image.size.width 111 | cropSize:self.photoView.cropView.frame.size 112 | imageViewSize:self.photoView.photoContentView.bounds.size]; 113 | 114 | UIImage *image = [UIImage imageWithCGImage:imageRef]; 115 | CGImageRelease(imageRef); 116 | 117 | if (self.autoSaveToLibray) { 118 | UIImageWriteToSavedPhotosAlbum(image, self, @selector(image:finishedSavingWithError:contextInfo:), nil); 119 | } 120 | 121 | [self.delegate photoTweaksController:self didFinishWithCroppedImage:image]; 122 | } 123 | 124 | - (void)image:(UIImage *)image finishedSavingWithError:(NSError *)error contextInfo:(void *)contextInfo { 125 | if(error != nil) { 126 | NSLog(@"ERROR: %@",[error localizedDescription]); 127 | } 128 | } 129 | 130 | - (CGImageRef)newScaledImage:(CGImageRef)source withOrientation:(UIImageOrientation)orientation toSize:(CGSize)size withQuality:(CGInterpolationQuality)quality 131 | { 132 | CGSize srcSize = size; 133 | CGFloat rotation = 0.0; 134 | 135 | switch(orientation) 136 | { 137 | case UIImageOrientationUp: { 138 | rotation = 0; 139 | } break; 140 | case UIImageOrientationDown: { 141 | rotation = M_PI; 142 | } break; 143 | case UIImageOrientationLeft:{ 144 | rotation = M_PI_2; 145 | srcSize = CGSizeMake(size.height, size.width); 146 | } break; 147 | case UIImageOrientationRight: { 148 | rotation = -M_PI_2; 149 | srcSize = CGSizeMake(size.height, size.width); 150 | } break; 151 | default: 152 | break; 153 | } 154 | 155 | CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB(); 156 | 157 | CGContextRef context = CGBitmapContextCreate(NULL, 158 | size.width, 159 | size.height, 160 | 8, //CGImageGetBitsPerComponent(source), 161 | 0, 162 | rgbColorSpace,//CGImageGetColorSpace(source), 163 | kCGImageAlphaPremultipliedLast|kCGBitmapByteOrder32Big//(CGBitmapInfo)kCGImageAlphaNoneSkipFirst //CGImageGetBitmapInfo(source) 164 | ); 165 | CGColorSpaceRelease(rgbColorSpace); 166 | 167 | CGContextSetInterpolationQuality(context, quality); 168 | CGContextTranslateCTM(context, size.width/2, size.height/2); 169 | CGContextRotateCTM(context,rotation); 170 | 171 | CGContextDrawImage(context, CGRectMake(-srcSize.width/2 , 172 | -srcSize.height/2, 173 | srcSize.width, 174 | srcSize.height), 175 | source); 176 | 177 | CGImageRef resultRef = CGBitmapContextCreateImage(context); 178 | CGContextRelease(context); 179 | 180 | return resultRef; 181 | } 182 | 183 | - (CGImageRef)newTransformedImage:(CGAffineTransform)transform 184 | sourceImage:(CGImageRef)sourceImage 185 | sourceSize:(CGSize)sourceSize 186 | sourceOrientation:(UIImageOrientation)sourceOrientation 187 | outputWidth:(CGFloat)outputWidth 188 | cropSize:(CGSize)cropSize 189 | imageViewSize:(CGSize)imageViewSize 190 | { 191 | CGImageRef source = [self newScaledImage:sourceImage 192 | withOrientation:sourceOrientation 193 | toSize:sourceSize 194 | withQuality:kCGInterpolationNone]; 195 | 196 | CGFloat aspect = cropSize.height/cropSize.width; 197 | CGSize outputSize = CGSizeMake(outputWidth, outputWidth*aspect); 198 | 199 | CGContextRef context = CGBitmapContextCreate(NULL, 200 | outputSize.width, 201 | outputSize.height, 202 | CGImageGetBitsPerComponent(source), 203 | 0, 204 | CGImageGetColorSpace(source), 205 | CGImageGetBitmapInfo(source)); 206 | CGContextSetFillColorWithColor(context, [[UIColor clearColor] CGColor]); 207 | CGContextFillRect(context, CGRectMake(0, 0, outputSize.width, outputSize.height)); 208 | 209 | CGAffineTransform uiCoords = CGAffineTransformMakeScale(outputSize.width / cropSize.width, 210 | outputSize.height / cropSize.height); 211 | uiCoords = CGAffineTransformTranslate(uiCoords, cropSize.width/2.0, cropSize.height / 2.0); 212 | uiCoords = CGAffineTransformScale(uiCoords, 1.0, -1.0); 213 | CGContextConcatCTM(context, uiCoords); 214 | 215 | CGContextConcatCTM(context, transform); 216 | CGContextScaleCTM(context, 1.0, -1.0); 217 | 218 | CGContextDrawImage(context, CGRectMake(-imageViewSize.width/2.0, 219 | -imageViewSize.height/2.0, 220 | imageViewSize.width, 221 | imageViewSize.height) 222 | , source); 223 | 224 | CGImageRef resultRef = CGBitmapContextCreateImage(context); 225 | CGContextRelease(context); 226 | CGImageRelease(source); 227 | return resultRef; 228 | } 229 | 230 | - (UIStatusBarStyle)preferredStatusBarStyle 231 | { 232 | return UIStatusBarStyleLightContent; 233 | } 234 | 235 | - (void)didReceiveMemoryWarning 236 | { 237 | [super didReceiveMemoryWarning]; 238 | } 239 | 240 | @end 241 | -------------------------------------------------------------------------------- /PhotoTweaks.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 00B1B9281ACAA90100A8E285 /* PhotoTweaks.strings in Resources */ = {isa = PBXBuildFile; fileRef = 00B1B92A1ACAA90100A8E285 /* PhotoTweaks.strings */; }; 11 | B71706151A32A18D00A6CC10 /* UIColor+Tweak.m in Sources */ = {isa = PBXBuildFile; fileRef = B71706141A32A18D00A6CC10 /* UIColor+Tweak.m */; }; 12 | CB3496FC1A3207E800C7EB0E /* PhotoTweaksViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CB3496FB1A3207E800C7EB0E /* PhotoTweaksViewController.m */; }; 13 | CBD5FA591A28425300D4D520 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = CBD5FA581A28425300D4D520 /* main.m */; }; 14 | CBD5FA5C1A28425300D4D520 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = CBD5FA5B1A28425300D4D520 /* AppDelegate.m */; }; 15 | CBD5FA621A28425300D4D520 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = CBD5FA601A28425300D4D520 /* Main.storyboard */; }; 16 | CBD5FA641A28425300D4D520 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CBD5FA631A28425300D4D520 /* Images.xcassets */; }; 17 | CBD5FA671A28425300D4D520 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = CBD5FA651A28425300D4D520 /* LaunchScreen.xib */; }; 18 | CBD5FA731A28425300D4D520 /* PhotoTweaksTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CBD5FA721A28425300D4D520 /* PhotoTweaksTests.m */; }; 19 | CBDE57B71A2CAA0700285CFF /* PhotoTweakView.m in Sources */ = {isa = PBXBuildFile; fileRef = CBDE57B61A2CAA0600285CFF /* PhotoTweakView.m */; }; 20 | /* End PBXBuildFile section */ 21 | 22 | /* Begin PBXContainerItemProxy section */ 23 | CBD5FA6D1A28425300D4D520 /* PBXContainerItemProxy */ = { 24 | isa = PBXContainerItemProxy; 25 | containerPortal = CBD5FA4B1A28425300D4D520 /* Project object */; 26 | proxyType = 1; 27 | remoteGlobalIDString = CBD5FA521A28425300D4D520; 28 | remoteInfo = PhotoTweaks; 29 | }; 30 | /* End PBXContainerItemProxy section */ 31 | 32 | /* Begin PBXFileReference section */ 33 | 00B1B9291ACAA90100A8E285 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/PhotoTweaks.strings; sourceTree = ""; }; 34 | 00B1B9361ACAA98400A8E285 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/PhotoTweaks.strings"; sourceTree = ""; }; 35 | B71706131A32A18D00A6CC10 /* UIColor+Tweak.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIColor+Tweak.h"; sourceTree = ""; }; 36 | B71706141A32A18D00A6CC10 /* UIColor+Tweak.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIColor+Tweak.m"; sourceTree = ""; }; 37 | CB3496FA1A3207E800C7EB0E /* PhotoTweaksViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PhotoTweaksViewController.h; sourceTree = ""; }; 38 | CB3496FB1A3207E800C7EB0E /* PhotoTweaksViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PhotoTweaksViewController.m; sourceTree = ""; }; 39 | CBD5FA531A28425300D4D520 /* PhotoTweaks.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PhotoTweaks.app; sourceTree = BUILT_PRODUCTS_DIR; }; 40 | CBD5FA571A28425300D4D520 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 41 | CBD5FA581A28425300D4D520 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 42 | CBD5FA5A1A28425300D4D520 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 43 | CBD5FA5B1A28425300D4D520 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 44 | CBD5FA611A28425300D4D520 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 45 | CBD5FA631A28425300D4D520 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 46 | CBD5FA661A28425300D4D520 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; }; 47 | CBD5FA6C1A28425300D4D520 /* PhotoTweaksTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = PhotoTweaksTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 48 | CBD5FA711A28425300D4D520 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 49 | CBD5FA721A28425300D4D520 /* PhotoTweaksTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PhotoTweaksTests.m; sourceTree = ""; }; 50 | CBDE57B51A2CAA0600285CFF /* PhotoTweakView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PhotoTweakView.h; sourceTree = ""; }; 51 | CBDE57B61A2CAA0600285CFF /* PhotoTweakView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PhotoTweakView.m; sourceTree = ""; }; 52 | /* End PBXFileReference section */ 53 | 54 | /* Begin PBXFrameworksBuildPhase section */ 55 | CBD5FA501A28425300D4D520 /* Frameworks */ = { 56 | isa = PBXFrameworksBuildPhase; 57 | buildActionMask = 2147483647; 58 | files = ( 59 | ); 60 | runOnlyForDeploymentPostprocessing = 0; 61 | }; 62 | CBD5FA691A28425300D4D520 /* Frameworks */ = { 63 | isa = PBXFrameworksBuildPhase; 64 | buildActionMask = 2147483647; 65 | files = ( 66 | ); 67 | runOnlyForDeploymentPostprocessing = 0; 68 | }; 69 | /* End PBXFrameworksBuildPhase section */ 70 | 71 | /* Begin PBXGroup section */ 72 | CBD5FA4A1A28425300D4D520 = { 73 | isa = PBXGroup; 74 | children = ( 75 | CBD5FA551A28425300D4D520 /* PhotoTweaks */, 76 | CBD5FA6F1A28425300D4D520 /* PhotoTweaksTests */, 77 | CBD5FA541A28425300D4D520 /* Products */, 78 | ); 79 | sourceTree = ""; 80 | }; 81 | CBD5FA541A28425300D4D520 /* Products */ = { 82 | isa = PBXGroup; 83 | children = ( 84 | CBD5FA531A28425300D4D520 /* PhotoTweaks.app */, 85 | CBD5FA6C1A28425300D4D520 /* PhotoTweaksTests.xctest */, 86 | ); 87 | name = Products; 88 | sourceTree = ""; 89 | }; 90 | CBD5FA551A28425300D4D520 /* PhotoTweaks */ = { 91 | isa = PBXGroup; 92 | children = ( 93 | CBDE57B41A2CA9EE00285CFF /* PhotoTweaks */, 94 | CBD5FA5A1A28425300D4D520 /* AppDelegate.h */, 95 | CBD5FA5B1A28425300D4D520 /* AppDelegate.m */, 96 | CBD5FA601A28425300D4D520 /* Main.storyboard */, 97 | CBD5FA631A28425300D4D520 /* Images.xcassets */, 98 | CBD5FA651A28425300D4D520 /* LaunchScreen.xib */, 99 | CBD5FA561A28425300D4D520 /* Supporting Files */, 100 | ); 101 | path = PhotoTweaks; 102 | sourceTree = ""; 103 | }; 104 | CBD5FA561A28425300D4D520 /* Supporting Files */ = { 105 | isa = PBXGroup; 106 | children = ( 107 | CBD5FA571A28425300D4D520 /* Info.plist */, 108 | CBD5FA581A28425300D4D520 /* main.m */, 109 | 00B1B92A1ACAA90100A8E285 /* PhotoTweaks.strings */, 110 | ); 111 | name = "Supporting Files"; 112 | sourceTree = ""; 113 | }; 114 | CBD5FA6F1A28425300D4D520 /* PhotoTweaksTests */ = { 115 | isa = PBXGroup; 116 | children = ( 117 | CBD5FA721A28425300D4D520 /* PhotoTweaksTests.m */, 118 | CBD5FA701A28425300D4D520 /* Supporting Files */, 119 | ); 120 | path = PhotoTweaksTests; 121 | sourceTree = ""; 122 | }; 123 | CBD5FA701A28425300D4D520 /* Supporting Files */ = { 124 | isa = PBXGroup; 125 | children = ( 126 | CBD5FA711A28425300D4D520 /* Info.plist */, 127 | ); 128 | name = "Supporting Files"; 129 | sourceTree = ""; 130 | }; 131 | CBDE57B41A2CA9EE00285CFF /* PhotoTweaks */ = { 132 | isa = PBXGroup; 133 | children = ( 134 | CB3496FA1A3207E800C7EB0E /* PhotoTweaksViewController.h */, 135 | CB3496FB1A3207E800C7EB0E /* PhotoTweaksViewController.m */, 136 | CBDE57B51A2CAA0600285CFF /* PhotoTweakView.h */, 137 | CBDE57B61A2CAA0600285CFF /* PhotoTweakView.m */, 138 | B71706131A32A18D00A6CC10 /* UIColor+Tweak.h */, 139 | B71706141A32A18D00A6CC10 /* UIColor+Tweak.m */, 140 | ); 141 | path = PhotoTweaks; 142 | sourceTree = ""; 143 | }; 144 | /* End PBXGroup section */ 145 | 146 | /* Begin PBXNativeTarget section */ 147 | CBD5FA521A28425300D4D520 /* PhotoTweaks */ = { 148 | isa = PBXNativeTarget; 149 | buildConfigurationList = CBD5FA761A28425300D4D520 /* Build configuration list for PBXNativeTarget "PhotoTweaks" */; 150 | buildPhases = ( 151 | CBD5FA4F1A28425300D4D520 /* Sources */, 152 | CBD5FA501A28425300D4D520 /* Frameworks */, 153 | CBD5FA511A28425300D4D520 /* Resources */, 154 | ); 155 | buildRules = ( 156 | ); 157 | dependencies = ( 158 | ); 159 | name = PhotoTweaks; 160 | productName = PhotoTweaks; 161 | productReference = CBD5FA531A28425300D4D520 /* PhotoTweaks.app */; 162 | productType = "com.apple.product-type.application"; 163 | }; 164 | CBD5FA6B1A28425300D4D520 /* PhotoTweaksTests */ = { 165 | isa = PBXNativeTarget; 166 | buildConfigurationList = CBD5FA791A28425300D4D520 /* Build configuration list for PBXNativeTarget "PhotoTweaksTests" */; 167 | buildPhases = ( 168 | CBD5FA681A28425300D4D520 /* Sources */, 169 | CBD5FA691A28425300D4D520 /* Frameworks */, 170 | CBD5FA6A1A28425300D4D520 /* Resources */, 171 | ); 172 | buildRules = ( 173 | ); 174 | dependencies = ( 175 | CBD5FA6E1A28425300D4D520 /* PBXTargetDependency */, 176 | ); 177 | name = PhotoTweaksTests; 178 | productName = PhotoTweaksTests; 179 | productReference = CBD5FA6C1A28425300D4D520 /* PhotoTweaksTests.xctest */; 180 | productType = "com.apple.product-type.bundle.unit-test"; 181 | }; 182 | /* End PBXNativeTarget section */ 183 | 184 | /* Begin PBXProject section */ 185 | CBD5FA4B1A28425300D4D520 /* Project object */ = { 186 | isa = PBXProject; 187 | attributes = { 188 | LastUpgradeCheck = 0610; 189 | ORGANIZATIONNAME = "Tu You"; 190 | TargetAttributes = { 191 | CBD5FA521A28425300D4D520 = { 192 | CreatedOnToolsVersion = 6.1; 193 | }; 194 | CBD5FA6B1A28425300D4D520 = { 195 | CreatedOnToolsVersion = 6.1; 196 | TestTargetID = CBD5FA521A28425300D4D520; 197 | }; 198 | }; 199 | }; 200 | buildConfigurationList = CBD5FA4E1A28425300D4D520 /* Build configuration list for PBXProject "PhotoTweaks" */; 201 | compatibilityVersion = "Xcode 3.2"; 202 | developmentRegion = English; 203 | hasScannedForEncodings = 0; 204 | knownRegions = ( 205 | en, 206 | Base, 207 | "zh-Hans", 208 | ); 209 | mainGroup = CBD5FA4A1A28425300D4D520; 210 | productRefGroup = CBD5FA541A28425300D4D520 /* Products */; 211 | projectDirPath = ""; 212 | projectRoot = ""; 213 | targets = ( 214 | CBD5FA521A28425300D4D520 /* PhotoTweaks */, 215 | CBD5FA6B1A28425300D4D520 /* PhotoTweaksTests */, 216 | ); 217 | }; 218 | /* End PBXProject section */ 219 | 220 | /* Begin PBXResourcesBuildPhase section */ 221 | CBD5FA511A28425300D4D520 /* Resources */ = { 222 | isa = PBXResourcesBuildPhase; 223 | buildActionMask = 2147483647; 224 | files = ( 225 | 00B1B9281ACAA90100A8E285 /* PhotoTweaks.strings in Resources */, 226 | CBD5FA621A28425300D4D520 /* Main.storyboard in Resources */, 227 | CBD5FA671A28425300D4D520 /* LaunchScreen.xib in Resources */, 228 | CBD5FA641A28425300D4D520 /* Images.xcassets in Resources */, 229 | ); 230 | runOnlyForDeploymentPostprocessing = 0; 231 | }; 232 | CBD5FA6A1A28425300D4D520 /* Resources */ = { 233 | isa = PBXResourcesBuildPhase; 234 | buildActionMask = 2147483647; 235 | files = ( 236 | ); 237 | runOnlyForDeploymentPostprocessing = 0; 238 | }; 239 | /* End PBXResourcesBuildPhase section */ 240 | 241 | /* Begin PBXSourcesBuildPhase section */ 242 | CBD5FA4F1A28425300D4D520 /* Sources */ = { 243 | isa = PBXSourcesBuildPhase; 244 | buildActionMask = 2147483647; 245 | files = ( 246 | CBD5FA5C1A28425300D4D520 /* AppDelegate.m in Sources */, 247 | B71706151A32A18D00A6CC10 /* UIColor+Tweak.m in Sources */, 248 | CBD5FA591A28425300D4D520 /* main.m in Sources */, 249 | CB3496FC1A3207E800C7EB0E /* PhotoTweaksViewController.m in Sources */, 250 | CBDE57B71A2CAA0700285CFF /* PhotoTweakView.m in Sources */, 251 | ); 252 | runOnlyForDeploymentPostprocessing = 0; 253 | }; 254 | CBD5FA681A28425300D4D520 /* Sources */ = { 255 | isa = PBXSourcesBuildPhase; 256 | buildActionMask = 2147483647; 257 | files = ( 258 | CBD5FA731A28425300D4D520 /* PhotoTweaksTests.m in Sources */, 259 | ); 260 | runOnlyForDeploymentPostprocessing = 0; 261 | }; 262 | /* End PBXSourcesBuildPhase section */ 263 | 264 | /* Begin PBXTargetDependency section */ 265 | CBD5FA6E1A28425300D4D520 /* PBXTargetDependency */ = { 266 | isa = PBXTargetDependency; 267 | target = CBD5FA521A28425300D4D520 /* PhotoTweaks */; 268 | targetProxy = CBD5FA6D1A28425300D4D520 /* PBXContainerItemProxy */; 269 | }; 270 | /* End PBXTargetDependency section */ 271 | 272 | /* Begin PBXVariantGroup section */ 273 | 00B1B92A1ACAA90100A8E285 /* PhotoTweaks.strings */ = { 274 | isa = PBXVariantGroup; 275 | children = ( 276 | 00B1B9291ACAA90100A8E285 /* en */, 277 | 00B1B9361ACAA98400A8E285 /* zh-Hans */, 278 | ); 279 | name = PhotoTweaks.strings; 280 | sourceTree = ""; 281 | }; 282 | CBD5FA601A28425300D4D520 /* Main.storyboard */ = { 283 | isa = PBXVariantGroup; 284 | children = ( 285 | CBD5FA611A28425300D4D520 /* Base */, 286 | ); 287 | name = Main.storyboard; 288 | sourceTree = ""; 289 | }; 290 | CBD5FA651A28425300D4D520 /* LaunchScreen.xib */ = { 291 | isa = PBXVariantGroup; 292 | children = ( 293 | CBD5FA661A28425300D4D520 /* Base */, 294 | ); 295 | name = LaunchScreen.xib; 296 | sourceTree = ""; 297 | }; 298 | /* End PBXVariantGroup section */ 299 | 300 | /* Begin XCBuildConfiguration section */ 301 | CBD5FA741A28425300D4D520 /* Debug */ = { 302 | isa = XCBuildConfiguration; 303 | buildSettings = { 304 | ALWAYS_SEARCH_USER_PATHS = NO; 305 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 306 | CLANG_CXX_LIBRARY = "libc++"; 307 | CLANG_ENABLE_MODULES = YES; 308 | CLANG_ENABLE_OBJC_ARC = YES; 309 | CLANG_WARN_BOOL_CONVERSION = YES; 310 | CLANG_WARN_CONSTANT_CONVERSION = YES; 311 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 312 | CLANG_WARN_EMPTY_BODY = YES; 313 | CLANG_WARN_ENUM_CONVERSION = YES; 314 | CLANG_WARN_INT_CONVERSION = YES; 315 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 316 | CLANG_WARN_UNREACHABLE_CODE = YES; 317 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 318 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 319 | COPY_PHASE_STRIP = NO; 320 | ENABLE_STRICT_OBJC_MSGSEND = YES; 321 | GCC_C_LANGUAGE_STANDARD = gnu99; 322 | GCC_DYNAMIC_NO_PIC = NO; 323 | GCC_OPTIMIZATION_LEVEL = 0; 324 | GCC_PREPROCESSOR_DEFINITIONS = ( 325 | "DEBUG=1", 326 | "$(inherited)", 327 | ); 328 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 329 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 330 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 331 | GCC_WARN_UNDECLARED_SELECTOR = YES; 332 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 333 | GCC_WARN_UNUSED_FUNCTION = YES; 334 | GCC_WARN_UNUSED_VARIABLE = YES; 335 | IPHONEOS_DEPLOYMENT_TARGET = 8.1; 336 | MTL_ENABLE_DEBUG_INFO = YES; 337 | ONLY_ACTIVE_ARCH = YES; 338 | SDKROOT = iphoneos; 339 | TARGETED_DEVICE_FAMILY = "1,2"; 340 | }; 341 | name = Debug; 342 | }; 343 | CBD5FA751A28425300D4D520 /* Release */ = { 344 | isa = XCBuildConfiguration; 345 | buildSettings = { 346 | ALWAYS_SEARCH_USER_PATHS = NO; 347 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 348 | CLANG_CXX_LIBRARY = "libc++"; 349 | CLANG_ENABLE_MODULES = YES; 350 | CLANG_ENABLE_OBJC_ARC = YES; 351 | CLANG_WARN_BOOL_CONVERSION = YES; 352 | CLANG_WARN_CONSTANT_CONVERSION = YES; 353 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 354 | CLANG_WARN_EMPTY_BODY = YES; 355 | CLANG_WARN_ENUM_CONVERSION = YES; 356 | CLANG_WARN_INT_CONVERSION = YES; 357 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 358 | CLANG_WARN_UNREACHABLE_CODE = YES; 359 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 360 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 361 | COPY_PHASE_STRIP = YES; 362 | ENABLE_NS_ASSERTIONS = NO; 363 | ENABLE_STRICT_OBJC_MSGSEND = YES; 364 | GCC_C_LANGUAGE_STANDARD = gnu99; 365 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 366 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 367 | GCC_WARN_UNDECLARED_SELECTOR = YES; 368 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 369 | GCC_WARN_UNUSED_FUNCTION = YES; 370 | GCC_WARN_UNUSED_VARIABLE = YES; 371 | IPHONEOS_DEPLOYMENT_TARGET = 8.1; 372 | MTL_ENABLE_DEBUG_INFO = NO; 373 | SDKROOT = iphoneos; 374 | TARGETED_DEVICE_FAMILY = "1,2"; 375 | VALIDATE_PRODUCT = YES; 376 | }; 377 | name = Release; 378 | }; 379 | CBD5FA771A28425300D4D520 /* Debug */ = { 380 | isa = XCBuildConfiguration; 381 | buildSettings = { 382 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 383 | INFOPLIST_FILE = PhotoTweaks/Info.plist; 384 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 385 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 386 | PRODUCT_NAME = "$(TARGET_NAME)"; 387 | TARGETED_DEVICE_FAMILY = 1; 388 | }; 389 | name = Debug; 390 | }; 391 | CBD5FA781A28425300D4D520 /* Release */ = { 392 | isa = XCBuildConfiguration; 393 | buildSettings = { 394 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 395 | INFOPLIST_FILE = PhotoTweaks/Info.plist; 396 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 397 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 398 | PRODUCT_NAME = "$(TARGET_NAME)"; 399 | TARGETED_DEVICE_FAMILY = 1; 400 | }; 401 | name = Release; 402 | }; 403 | CBD5FA7A1A28425300D4D520 /* Debug */ = { 404 | isa = XCBuildConfiguration; 405 | buildSettings = { 406 | BUNDLE_LOADER = "$(TEST_HOST)"; 407 | FRAMEWORK_SEARCH_PATHS = ( 408 | "$(SDKROOT)/Developer/Library/Frameworks", 409 | "$(inherited)", 410 | ); 411 | GCC_PREPROCESSOR_DEFINITIONS = ( 412 | "DEBUG=1", 413 | "$(inherited)", 414 | ); 415 | INFOPLIST_FILE = PhotoTweaksTests/Info.plist; 416 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 417 | PRODUCT_NAME = "$(TARGET_NAME)"; 418 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PhotoTweaks.app/PhotoTweaks"; 419 | }; 420 | name = Debug; 421 | }; 422 | CBD5FA7B1A28425300D4D520 /* Release */ = { 423 | isa = XCBuildConfiguration; 424 | buildSettings = { 425 | BUNDLE_LOADER = "$(TEST_HOST)"; 426 | FRAMEWORK_SEARCH_PATHS = ( 427 | "$(SDKROOT)/Developer/Library/Frameworks", 428 | "$(inherited)", 429 | ); 430 | INFOPLIST_FILE = PhotoTweaksTests/Info.plist; 431 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 432 | PRODUCT_NAME = "$(TARGET_NAME)"; 433 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PhotoTweaks.app/PhotoTweaks"; 434 | }; 435 | name = Release; 436 | }; 437 | /* End XCBuildConfiguration section */ 438 | 439 | /* Begin XCConfigurationList section */ 440 | CBD5FA4E1A28425300D4D520 /* Build configuration list for PBXProject "PhotoTweaks" */ = { 441 | isa = XCConfigurationList; 442 | buildConfigurations = ( 443 | CBD5FA741A28425300D4D520 /* Debug */, 444 | CBD5FA751A28425300D4D520 /* Release */, 445 | ); 446 | defaultConfigurationIsVisible = 0; 447 | defaultConfigurationName = Release; 448 | }; 449 | CBD5FA761A28425300D4D520 /* Build configuration list for PBXNativeTarget "PhotoTweaks" */ = { 450 | isa = XCConfigurationList; 451 | buildConfigurations = ( 452 | CBD5FA771A28425300D4D520 /* Debug */, 453 | CBD5FA781A28425300D4D520 /* Release */, 454 | ); 455 | defaultConfigurationIsVisible = 0; 456 | defaultConfigurationName = Release; 457 | }; 458 | CBD5FA791A28425300D4D520 /* Build configuration list for PBXNativeTarget "PhotoTweaksTests" */ = { 459 | isa = XCConfigurationList; 460 | buildConfigurations = ( 461 | CBD5FA7A1A28425300D4D520 /* Debug */, 462 | CBD5FA7B1A28425300D4D520 /* Release */, 463 | ); 464 | defaultConfigurationIsVisible = 0; 465 | defaultConfigurationName = Release; 466 | }; 467 | /* End XCConfigurationList section */ 468 | }; 469 | rootObject = CBD5FA4B1A28425300D4D520 /* Project object */; 470 | } 471 | -------------------------------------------------------------------------------- /PhotoTweaks/PhotoTweaks/PhotoTweakView.m: -------------------------------------------------------------------------------- 1 | // 2 | // PhotoView.m 3 | // PhotoTweaks 4 | // 5 | // Created by Tu You on 14/12/2. 6 | // Copyright (c) 2014年 Tu You. All rights reserved. 7 | // 8 | 9 | #import "PhotoTweakView.h" 10 | #import "UIColor+Tweak.h" 11 | #import 12 | 13 | const CGFloat kMaxRotationAngle = 0.5; 14 | static const NSUInteger kCropLines = 2; 15 | static const NSUInteger kGridLines = 9; 16 | 17 | static const CGFloat kCropViewHotArea = 16; 18 | static const CGFloat kMinimumCropArea = 60; 19 | static const CGFloat kMaximumCanvasWidthRatio = 0.9; 20 | static const CGFloat kMaximumCanvasHeightRatio = 0.7; 21 | static const CGFloat kCanvasHeaderHeigth = 60; 22 | static const CGFloat kCropViewCornerLength = 22; 23 | 24 | static CGFloat distanceBetweenPoints(CGPoint point0, CGPoint point1) 25 | { 26 | return sqrt(pow(point1.x - point0.x, 2) + pow(point1.y - point0.y, 2)); 27 | } 28 | 29 | //#define kInstruction 30 | 31 | @implementation PhotoContentView 32 | 33 | - (instancetype)initWithImage:(UIImage *)image 34 | { 35 | if (self = [super init]) { 36 | _image = image; 37 | 38 | self.frame = CGRectMake(0, 0, image.size.width, image.size.height); 39 | 40 | _imageView = [[UIImageView alloc] initWithFrame:self.bounds]; 41 | _imageView.image = self.image; 42 | _imageView.userInteractionEnabled = YES; 43 | [self addSubview:_imageView]; 44 | } 45 | return self; 46 | } 47 | 48 | - (void)layoutSubviews 49 | { 50 | [super layoutSubviews]; 51 | 52 | self.imageView.frame = self.bounds; 53 | } 54 | 55 | @end 56 | 57 | @interface PhotoScrollView : UIScrollView 58 | 59 | @property (nonatomic, strong) PhotoContentView *photoContentView; 60 | 61 | @end 62 | 63 | @implementation PhotoScrollView 64 | 65 | - (void)setContentOffsetY:(CGFloat)offsetY 66 | { 67 | CGPoint contentOffset = self.contentOffset; 68 | contentOffset.y = offsetY; 69 | self.contentOffset = contentOffset; 70 | } 71 | 72 | - (void)setContentOffsetX:(CGFloat)offsetX 73 | { 74 | CGPoint contentOffset = self.contentOffset; 75 | contentOffset.x = offsetX; 76 | self.contentOffset = contentOffset; 77 | } 78 | 79 | - (CGFloat)zoomScaleToBound 80 | { 81 | CGFloat scaleW = self.bounds.size.width / self.photoContentView.bounds.size.width; 82 | CGFloat scaleH = self.bounds.size.height / self.photoContentView.bounds.size.height; 83 | CGFloat max = MAX(scaleW, scaleH); 84 | 85 | return max; 86 | } 87 | 88 | @end 89 | 90 | typedef NS_ENUM(NSInteger, CropCornerType) { 91 | CropCornerTypeUpperLeft, 92 | CropCornerTypeUpperRight, 93 | CropCornerTypeLowerRight, 94 | CropCornerTypeLowerLeft 95 | }; 96 | 97 | @interface CropCornerView : UIView 98 | 99 | @end 100 | 101 | @implementation CropCornerView 102 | 103 | - (instancetype)initWithCornerType:(CropCornerType)type 104 | { 105 | if (self = [super init]) { 106 | self.frame = CGRectMake(0, 0, kCropViewCornerLength, kCropViewCornerLength); 107 | self.backgroundColor = [UIColor clearColor]; 108 | 109 | CGFloat lineWidth = 2; 110 | UIView *horizontal = [[UIView alloc] initWithFrame:CGRectMake(0, 0, kCropViewCornerLength, lineWidth)]; 111 | horizontal.backgroundColor = [UIColor cropLineColor]; 112 | [self addSubview:horizontal]; 113 | 114 | UIView *vertical = [[UIView alloc] initWithFrame:CGRectMake(0, 0, lineWidth, kCropViewCornerLength)]; 115 | vertical.backgroundColor = [UIColor cropLineColor]; 116 | [self addSubview:vertical]; 117 | 118 | if (type == CropCornerTypeUpperLeft) { 119 | horizontal.center = CGPointMake(kCropViewCornerLength / 2, lineWidth / 2); 120 | vertical.center = CGPointMake(lineWidth / 2, kCropViewCornerLength / 2); 121 | } else if (type == CropCornerTypeUpperRight) { 122 | horizontal.center = CGPointMake(kCropViewCornerLength / 2, lineWidth / 2); 123 | vertical.center = CGPointMake(kCropViewCornerLength - lineWidth / 2, kCropViewCornerLength / 2); 124 | } else if (type == CropCornerTypeLowerRight) { 125 | horizontal.center = CGPointMake(kCropViewCornerLength / 2, kCropViewCornerLength - lineWidth / 2); 126 | vertical.center = CGPointMake(kCropViewCornerLength - lineWidth / 2, kCropViewCornerLength / 2); 127 | } else if (type == CropCornerTypeLowerLeft) { 128 | horizontal.center = CGPointMake(kCropViewCornerLength / 2, kCropViewCornerLength - lineWidth / 2); 129 | vertical.center = CGPointMake(lineWidth / 2, kCropViewCornerLength / 2); 130 | } 131 | } 132 | return self; 133 | } 134 | 135 | @end 136 | 137 | @interface CropView () 138 | 139 | @property (nonatomic, strong) CropCornerView *upperLeft; 140 | @property (nonatomic, strong) CropCornerView *upperRight; 141 | @property (nonatomic, strong) CropCornerView *lowerRight; 142 | @property (nonatomic, strong) CropCornerView *lowerLeft; 143 | 144 | @property (nonatomic, strong) NSMutableArray *horizontalCropLines; 145 | @property (nonatomic, strong) NSMutableArray *verticalCropLines; 146 | 147 | @property (nonatomic, strong) NSMutableArray *horizontalGridLines; 148 | @property (nonatomic, strong) NSMutableArray *verticalGridLines; 149 | 150 | @property (nonatomic, weak) id delegate; 151 | 152 | @property (nonatomic, assign) BOOL cropLinesDismissed; 153 | @property (nonatomic, assign) BOOL gridLinesDismissed; 154 | 155 | @end 156 | 157 | @implementation CropView 158 | 159 | - (instancetype)initWithFrame:(CGRect)frame 160 | { 161 | if (self = [super initWithFrame:frame]) { 162 | self.layer.borderColor = [UIColor cropLineColor].CGColor; 163 | self.layer.borderWidth = 1; 164 | 165 | _horizontalCropLines = [NSMutableArray array]; 166 | for (int i = 0; i < kCropLines; i++) { 167 | UIView *line = [UIView new]; 168 | line.backgroundColor = [UIColor cropLineColor]; 169 | [_horizontalCropLines addObject:line]; 170 | [self addSubview:line]; 171 | } 172 | 173 | _verticalCropLines = [NSMutableArray array]; 174 | for (int i = 0; i < kCropLines; i++) { 175 | UIView *line = [UIView new]; 176 | line.backgroundColor = [UIColor cropLineColor]; 177 | [_verticalCropLines addObject:line]; 178 | [self addSubview:line]; 179 | } 180 | 181 | _horizontalGridLines = [NSMutableArray array]; 182 | for (int i = 0; i < kGridLines; i++) { 183 | UIView *line = [UIView new]; 184 | line.backgroundColor = [UIColor gridLineColor]; 185 | [_horizontalGridLines addObject:line]; 186 | [self addSubview:line]; 187 | } 188 | 189 | _verticalGridLines = [NSMutableArray array]; 190 | for (int i = 0; i < kGridLines; i++) { 191 | UIView *line = [UIView new]; 192 | line.backgroundColor = [UIColor gridLineColor]; 193 | [_verticalGridLines addObject:line]; 194 | [self addSubview:line]; 195 | } 196 | 197 | _cropLinesDismissed = YES; 198 | _gridLinesDismissed = YES; 199 | 200 | _upperLeft = [[CropCornerView alloc] initWithCornerType:CropCornerTypeUpperLeft]; 201 | _upperLeft.center = CGPointMake(kCropViewCornerLength / 2, kCropViewCornerLength / 2); 202 | _upperLeft.autoresizingMask = UIViewAutoresizingNone; 203 | [self addSubview:_upperLeft]; 204 | 205 | _upperRight = [[CropCornerView alloc] initWithCornerType:CropCornerTypeUpperRight]; 206 | _upperRight.center = CGPointMake(self.frame.size.width - kCropViewCornerLength / 2, kCropViewCornerLength / 2); 207 | _upperRight.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin; 208 | [self addSubview:_upperRight]; 209 | 210 | _lowerRight = [[CropCornerView alloc] initWithCornerType:CropCornerTypeLowerRight]; 211 | _lowerRight.center = CGPointMake(self.frame.size.width - kCropViewCornerLength / 2, self.frame.size.height - kCropViewCornerLength / 2); 212 | _lowerRight.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleLeftMargin; 213 | [self addSubview:_lowerRight]; 214 | 215 | _lowerLeft = [[CropCornerView alloc] initWithCornerType:CropCornerTypeLowerLeft]; 216 | _lowerLeft.center = CGPointMake(kCropViewCornerLength / 2, self.frame.size.height - kCropViewCornerLength / 2); 217 | _lowerLeft.autoresizingMask = UIViewAutoresizingFlexibleTopMargin; 218 | [self addSubview:_lowerLeft]; 219 | } 220 | return self; 221 | } 222 | 223 | - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event 224 | { 225 | if ([touches count] == 1) { 226 | [self updateCropLines:NO]; 227 | } 228 | } 229 | 230 | - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event 231 | { 232 | if ([touches count] == 1) { 233 | CGPoint location = [[touches anyObject] locationInView:self]; 234 | CGRect frame = self.frame; 235 | 236 | CGPoint p0 = CGPointMake(0, 0); 237 | CGPoint p1 = CGPointMake(self.frame.size.width, 0); 238 | CGPoint p2 = CGPointMake(0, self.frame.size.height); 239 | CGPoint p3 = CGPointMake(self.frame.size.width, self.frame.size.height); 240 | 241 | BOOL canChangeWidth = frame.size.width > kMinimumCropArea; 242 | BOOL canChangeHeight = frame.size.height > kMinimumCropArea; 243 | 244 | if (distanceBetweenPoints(location, p0) < kCropViewHotArea) { 245 | if (canChangeWidth) { 246 | frame.origin.x += location.x; 247 | frame.size.width -= location.x; 248 | } 249 | if (canChangeHeight) { 250 | frame.origin.y += location.y; 251 | frame.size.height -= location.y; 252 | } 253 | } else if (distanceBetweenPoints(location, p1) < kCropViewHotArea) { 254 | if (canChangeWidth) { 255 | frame.size.width = location.x; 256 | } 257 | if (canChangeHeight) { 258 | frame.origin.y += location.y; 259 | frame.size.height -= location.y; 260 | } 261 | } else if (distanceBetweenPoints(location, p2) < kCropViewHotArea) { 262 | if (canChangeWidth) { 263 | frame.origin.x += location.x; 264 | frame.size.width -= location.x; 265 | } 266 | if (canChangeHeight) { 267 | frame.size.height = location.y; 268 | } 269 | } else if (distanceBetweenPoints(location, p3) < kCropViewHotArea) { 270 | if (canChangeWidth) { 271 | frame.size.width = location.x; 272 | } 273 | if (canChangeHeight) { 274 | frame.size.height = location.y; 275 | } 276 | } else if (fabs(location.x - p0.x) < kCropViewHotArea) { 277 | if (canChangeWidth) { 278 | frame.origin.x += location.x; 279 | frame.size.width -= location.x; 280 | } 281 | } else if (fabs(location.x - p1.x) < kCropViewHotArea) { 282 | if (canChangeWidth) { 283 | frame.size.width = location.x; 284 | } 285 | } else if (fabs(location.y - p0.y) < kCropViewHotArea) { 286 | if (canChangeHeight) { 287 | frame.origin.y += location.y; 288 | frame.size.height -= location.y; 289 | } 290 | } else if (fabs(location.y - p2.y) < kCropViewHotArea) { 291 | if (canChangeHeight) { 292 | frame.size.height = location.y; 293 | } 294 | } 295 | 296 | self.frame = frame; 297 | 298 | // update crop lines 299 | [self updateCropLines:NO]; 300 | 301 | if ([self.delegate respondsToSelector:@selector(cropMoved:)]) { 302 | [self.delegate cropMoved:self]; 303 | } 304 | } 305 | } 306 | 307 | - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event 308 | { 309 | if ([self.delegate respondsToSelector:@selector(cropEnded:)]) { 310 | [self.delegate cropEnded:self]; 311 | } 312 | } 313 | 314 | - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event 315 | { 316 | 317 | } 318 | 319 | - (void)updateCropLines:(BOOL)animate 320 | { 321 | // show crop lines 322 | if (self.cropLinesDismissed) { 323 | [self showCropLines]; 324 | } 325 | 326 | void (^animationBlock)(void) = ^(void) { 327 | [self updateLines:self.horizontalCropLines horizontal:YES]; 328 | [self updateLines:self.verticalCropLines horizontal:NO]; 329 | }; 330 | 331 | if (animate) { 332 | [UIView animateWithDuration:0.25 animations:animationBlock]; 333 | } else { 334 | animationBlock(); 335 | } 336 | } 337 | 338 | - (void)updateGridLines:(BOOL)animate 339 | { 340 | // show grid lines 341 | if (self.gridLinesDismissed) { 342 | [self showGridLines]; 343 | } 344 | 345 | void (^animationBlock)(void) = ^(void) { 346 | 347 | [self updateLines:self.horizontalGridLines horizontal:YES]; 348 | [self updateLines:self.verticalGridLines horizontal:NO]; 349 | }; 350 | 351 | if (animate) { 352 | [UIView animateWithDuration:0.25 animations:animationBlock]; 353 | } else { 354 | animationBlock(); 355 | } 356 | } 357 | 358 | - (void)updateLines:(NSArray *)lines horizontal:(BOOL)horizontal 359 | { 360 | [lines enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { 361 | UIView *line = (UIView *)obj; 362 | if (horizontal) { 363 | line.frame = CGRectMake(0, 364 | (self.frame.size.height / (lines.count + 1)) * (idx + 1), 365 | self.frame.size.width, 366 | 1 / [UIScreen mainScreen].scale); 367 | } else { 368 | line.frame = CGRectMake((self.frame.size.width / (lines.count + 1)) * (idx + 1), 369 | 0, 370 | 1 / [UIScreen mainScreen].scale, 371 | self.frame.size.height); 372 | } 373 | }]; 374 | } 375 | 376 | - (void)dismissCropLines 377 | { 378 | [UIView animateWithDuration:0.2 animations:^{ 379 | [self dismissLines:self.horizontalCropLines]; 380 | [self dismissLines:self.verticalCropLines]; 381 | } completion:^(BOOL finished) { 382 | self.cropLinesDismissed = YES; 383 | }]; 384 | } 385 | 386 | - (void)dismissGridLines 387 | { 388 | [UIView animateWithDuration:0.2 animations:^{ 389 | [self dismissLines:self.horizontalGridLines]; 390 | [self dismissLines:self.verticalGridLines]; 391 | } completion:^(BOOL finished) { 392 | self.gridLinesDismissed = YES; 393 | }]; 394 | } 395 | 396 | - (void)dismissLines:(NSArray *)lines 397 | { 398 | [lines enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { 399 | ((UIView *)obj).alpha = 0.0f; 400 | }]; 401 | } 402 | 403 | - (void)showCropLines 404 | { 405 | self.cropLinesDismissed = NO; 406 | [UIView animateWithDuration:0.2 animations:^{ 407 | [self showLines:self.horizontalCropLines]; 408 | [self showLines:self.verticalCropLines]; 409 | }]; 410 | } 411 | 412 | - (void)showGridLines 413 | { 414 | self.gridLinesDismissed = NO; 415 | [UIView animateWithDuration:0.2 animations:^{ 416 | [self showLines:self.horizontalGridLines]; 417 | [self showLines:self.verticalGridLines]; 418 | }]; 419 | } 420 | 421 | - (void)showLines:(NSArray *)lines 422 | { 423 | [lines enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { 424 | ((UIView *)obj).alpha = 1.0f; 425 | }]; 426 | } 427 | 428 | @end 429 | 430 | @interface PhotoTweakView () 431 | 432 | @property (nonatomic, strong) PhotoScrollView *scrollView; 433 | @property (nonatomic, strong) CropView *cropView; 434 | 435 | @property (nonatomic, strong) UIImage *image; 436 | @property (nonatomic, strong) UISlider *slider; 437 | @property (nonatomic, strong) UIButton *resetBtn; 438 | @property (nonatomic, assign) CGSize originalSize; 439 | @property (nonatomic, assign) CGFloat angle; 440 | 441 | @property (nonatomic, assign) BOOL manualZoomed; 442 | 443 | // masks 444 | @property (nonatomic, strong) UIView *topMask; 445 | @property (nonatomic, strong) UIView *leftMask; 446 | @property (nonatomic, strong) UIView *bottomMask; 447 | @property (nonatomic, strong) UIView *rightMask; 448 | 449 | // constants 450 | @property (nonatomic, assign) CGSize maximumCanvasSize; 451 | @property (nonatomic, assign) CGFloat centerY; 452 | @property (nonatomic, assign) CGPoint originalPoint; 453 | @property (nonatomic, assign) CGFloat maxRotationAngle; 454 | 455 | @end 456 | 457 | @implementation PhotoTweakView 458 | 459 | - (instancetype)initWithFrame:(CGRect)frame 460 | image:(UIImage *)image 461 | maxRotationAngle:(CGFloat)maxRotationAngle 462 | { 463 | if (self = [super init]) { 464 | 465 | self.frame = frame; 466 | 467 | _image = image; 468 | _maxRotationAngle = maxRotationAngle; 469 | 470 | // scale the image 471 | _maximumCanvasSize = CGSizeMake(kMaximumCanvasWidthRatio * self.frame.size.width, 472 | kMaximumCanvasHeightRatio * self.frame.size.height - kCanvasHeaderHeigth); 473 | 474 | CGFloat scaleX = image.size.width / self.maximumCanvasSize.width; 475 | CGFloat scaleY = image.size.height / self.maximumCanvasSize.height; 476 | CGFloat scale = MAX(scaleX, scaleY); 477 | CGRect bounds = CGRectMake(0, 0, image.size.width / scale, image.size.height / scale); 478 | _originalSize = bounds.size; 479 | 480 | _centerY = self.maximumCanvasSize.height / 2 + kCanvasHeaderHeigth; 481 | 482 | _scrollView = [[PhotoScrollView alloc] initWithFrame:bounds]; 483 | _scrollView.center = CGPointMake(CGRectGetWidth(self.frame) / 2, self.centerY); 484 | _scrollView.bounces = YES; 485 | _scrollView.layer.anchorPoint = CGPointMake(0.5, 0.5); 486 | _scrollView.alwaysBounceVertical = YES; 487 | _scrollView.alwaysBounceHorizontal = YES; 488 | _scrollView.delegate = self; 489 | _scrollView.minimumZoomScale = 1; 490 | _scrollView.maximumZoomScale = 10; 491 | _scrollView.showsVerticalScrollIndicator = NO; 492 | _scrollView.showsHorizontalScrollIndicator = NO; 493 | _scrollView.clipsToBounds = NO; 494 | _scrollView.contentSize = CGSizeMake(self.scrollView.bounds.size.width, self.scrollView.bounds.size.height); 495 | [self addSubview:_scrollView]; 496 | #ifdef kInstruction 497 | _scrollView.layer.borderColor = [UIColor redColor].CGColor; 498 | _scrollView.layer.borderWidth = 1; 499 | _scrollView.showsVerticalScrollIndicator = YES; 500 | _scrollView.showsHorizontalScrollIndicator = YES; 501 | #endif 502 | 503 | _photoContentView = [[PhotoContentView alloc] initWithImage:image]; 504 | _photoContentView.frame = self.scrollView.bounds; 505 | _photoContentView.backgroundColor = [UIColor clearColor]; 506 | _photoContentView.userInteractionEnabled = YES; 507 | _scrollView.photoContentView = self.photoContentView; 508 | [self.scrollView addSubview:_photoContentView]; 509 | 510 | _cropView = [[CropView alloc] initWithFrame:self.scrollView.frame]; 511 | _cropView.center = self.scrollView.center; 512 | _cropView.delegate = self; 513 | [self addSubview:_cropView]; 514 | 515 | UIColor *maskColor = [UIColor maskColor]; 516 | _topMask = [UIView new]; 517 | _topMask.backgroundColor = maskColor; 518 | [self addSubview:_topMask]; 519 | _leftMask = [UIView new]; 520 | _leftMask.backgroundColor = maskColor; 521 | [self addSubview:_leftMask]; 522 | _bottomMask = [UIView new]; 523 | _bottomMask.backgroundColor = maskColor; 524 | [self addSubview:_bottomMask]; 525 | _rightMask = [UIView new]; 526 | _rightMask.backgroundColor = maskColor; 527 | [self addSubview:_rightMask]; 528 | [self updateMasks:NO]; 529 | 530 | _slider = [[UISlider alloc] initWithFrame:CGRectMake(0, 0, 260, 20)]; 531 | _slider.center = CGPointMake(CGRectGetWidth(self.bounds) / 2, CGRectGetHeight(self.bounds) - 135); 532 | _slider.minimumValue = -self.maxRotationAngle; 533 | _slider.maximumValue = self.maxRotationAngle; 534 | [_slider addTarget:self action:@selector(sliderValueChanged:) forControlEvents:UIControlEventValueChanged]; 535 | [_slider addTarget:self action:@selector(sliderTouchEnded:) forControlEvents:UIControlEventTouchUpInside]; 536 | [self addSubview:_slider]; 537 | 538 | _resetBtn = [UIButton buttonWithType:UIButtonTypeCustom]; 539 | _resetBtn.frame = CGRectMake(0, 0, 60, 20); 540 | _resetBtn.center = CGPointMake(CGRectGetWidth(self.bounds) / 2, CGRectGetHeight(self.bounds) - 95); 541 | _resetBtn.titleLabel.font = [UIFont systemFontOfSize:14]; 542 | [_resetBtn setTitleColor:[UIColor resetButtonColor] forState:UIControlStateNormal]; 543 | [_resetBtn setTitleColor:[UIColor resetButtonHighlightedColor] forState:UIControlStateHighlighted]; 544 | [_resetBtn setTitle:NSLocalizedStringFromTable(@"RESET", @"PhotoTweaks", nil) forState:UIControlStateNormal]; 545 | [_resetBtn addTarget:self action:@selector(resetBtnTapped:) forControlEvents:UIControlEventTouchUpInside]; 546 | [self addSubview:_resetBtn]; 547 | 548 | _originalPoint = [self convertPoint:self.scrollView.center toView:self]; 549 | } 550 | return self; 551 | } 552 | 553 | - (instancetype)initWithFrame:(CGRect)frame image:(UIImage *)image 554 | { 555 | return [self initWithFrame:frame image:image maxRotationAngle:kMaxRotationAngle]; 556 | } 557 | 558 | - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event 559 | { 560 | if (CGRectContainsPoint(self.slider.frame, point)) { 561 | return self.slider; 562 | } else if (CGRectContainsPoint(self.resetBtn.frame, point)) { 563 | return self.resetBtn; 564 | } else if (CGRectContainsPoint(CGRectInset(self.cropView.frame, -kCropViewHotArea, -kCropViewHotArea), point) && !CGRectContainsPoint(CGRectInset(self.cropView.frame, kCropViewHotArea, kCropViewHotArea), point)) { 565 | return self.cropView; 566 | } 567 | return self.scrollView; 568 | } 569 | 570 | - (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView 571 | { 572 | return self.photoContentView; 573 | } 574 | 575 | - (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(CGFloat)scale 576 | { 577 | self.manualZoomed = YES; 578 | } 579 | 580 | #pragma mark - Crop View Delegate 581 | 582 | - (void)cropMoved:(CropView *)cropView 583 | { 584 | [self updateMasks:NO]; 585 | } 586 | 587 | - (void)cropEnded:(CropView *)cropView 588 | { 589 | CGFloat scaleX = self.originalSize.width / cropView.bounds.size.width; 590 | CGFloat scaleY = self.originalSize.height / cropView.bounds.size.height; 591 | CGFloat scale = MIN(scaleX, scaleY); 592 | 593 | // calculate the new bounds of crop view 594 | CGRect newCropBounds = CGRectMake(0, 0, scale * cropView.frame.size.width, scale * cropView.frame.size.height); 595 | 596 | // calculate the new bounds of scroll view 597 | CGFloat width = fabs(cos(self.angle)) * newCropBounds.size.width + fabs(sin(self.angle)) * newCropBounds.size.height; 598 | CGFloat height = fabs(sin(self.angle)) * newCropBounds.size.width + fabs(cos(self.angle)) * newCropBounds.size.height; 599 | 600 | // calculate the zoom area of scroll view 601 | CGRect scaleFrame = cropView.frame; 602 | if (scaleFrame.size.width >= self.scrollView.bounds.size.width) { 603 | scaleFrame.size.width = self.scrollView.bounds.size.width - 1; 604 | } 605 | if (scaleFrame.size.height >= self.scrollView.bounds.size.height) { 606 | scaleFrame.size.height = self.scrollView.bounds.size.height - 1; 607 | } 608 | 609 | CGPoint contentOffset = self.scrollView.contentOffset; 610 | CGPoint contentOffsetCenter = CGPointMake(contentOffset.x + self.scrollView.bounds.size.width / 2, contentOffset.y + self.scrollView.bounds.size.height / 2); 611 | CGRect bounds = self.scrollView.bounds; 612 | bounds.size.width = width; 613 | bounds.size.height = height; 614 | self.scrollView.bounds = CGRectMake(0, 0, width, height); 615 | CGPoint newContentOffset = CGPointMake(contentOffsetCenter.x - self.scrollView.bounds.size.width / 2, contentOffsetCenter.y - self.scrollView.bounds.size.height / 2); 616 | self.scrollView.contentOffset = newContentOffset; 617 | 618 | [UIView animateWithDuration:0.25 animations:^{ 619 | // animate crop view 620 | cropView.bounds = CGRectMake(0, 0, newCropBounds.size.width, newCropBounds.size.height); 621 | cropView.center = CGPointMake(CGRectGetWidth(self.frame) / 2, self.centerY); 622 | 623 | // zoom the specified area of scroll view 624 | CGRect zoomRect = [self convertRect:scaleFrame toView:self.scrollView.photoContentView]; 625 | [self.scrollView zoomToRect:zoomRect animated:NO]; 626 | }]; 627 | 628 | self.manualZoomed = YES; 629 | 630 | // update masks 631 | [self updateMasks:YES]; 632 | 633 | [self.cropView dismissCropLines]; 634 | 635 | CGFloat scaleH = self.scrollView.bounds.size.height / self.scrollView.contentSize.height; 636 | CGFloat scaleW = self.scrollView.bounds.size.width / self.scrollView.contentSize.width; 637 | __block CGFloat scaleM = MAX(scaleH, scaleW); 638 | dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ 639 | if (scaleM > 1) { 640 | scaleM = scaleM * self.scrollView.zoomScale; 641 | [self.scrollView setZoomScale:scaleM animated:NO]; 642 | } 643 | [UIView animateWithDuration:0.2 animations:^{ 644 | [self checkScrollViewContentOffset]; 645 | }]; 646 | }); 647 | } 648 | 649 | - (void)updateMasks:(BOOL)animate 650 | { 651 | void (^animationBlock)(void) = ^(void) { 652 | self.topMask.frame = CGRectMake(0, 0, self.cropView.frame.origin.x + self.cropView.frame.size.width, self.cropView.frame.origin.y); 653 | self.leftMask.frame = CGRectMake(0, self.cropView.frame.origin.y, self.cropView.frame.origin.x, self.frame.size.height - self.cropView.frame.origin.y); 654 | self.bottomMask.frame = CGRectMake(self.cropView.frame.origin.x, self.cropView.frame.origin.y + self.cropView.frame.size.height, self.frame.size.width - self.cropView.frame.origin.x, self.frame.size.height - (self.cropView.frame.origin.y + self.cropView.frame.size.height)); 655 | self.rightMask.frame = CGRectMake(self.cropView.frame.origin.x + self.cropView.frame.size.width, 0, self.frame.size.width - (self.cropView.frame.origin.x + self.cropView.frame.size.width), self.cropView.frame.origin.y + self.cropView.frame.size.height); 656 | }; 657 | 658 | if (animate) { 659 | [UIView animateWithDuration:0.25 animations:animationBlock]; 660 | } else { 661 | animationBlock(); 662 | } 663 | } 664 | 665 | - (void)checkScrollViewContentOffset 666 | { 667 | self.scrollView.contentOffsetX = MAX(self.scrollView.contentOffset.x, 0); 668 | self.scrollView.contentOffsetY = MAX(self.scrollView.contentOffset.y, 0); 669 | 670 | if (self.scrollView.contentSize.height - self.scrollView.contentOffset.y <= self.scrollView.bounds.size.height) { 671 | self.scrollView.contentOffsetY = self.scrollView.contentSize.height - self.scrollView.bounds.size.height; 672 | } 673 | 674 | if (self.scrollView.contentSize.width - self.scrollView.contentOffset.x <= self.scrollView.bounds.size.width) { 675 | self.scrollView.contentOffsetX = self.scrollView.contentSize.width - self.scrollView.bounds.size.width; 676 | } 677 | } 678 | 679 | - (void)sliderValueChanged:(id)sender 680 | { 681 | // update masks 682 | [self updateMasks:NO]; 683 | 684 | // update grids 685 | [self.cropView updateGridLines:NO]; 686 | 687 | // rotate scroll view 688 | self.angle = self.slider.value; 689 | self.scrollView.transform = CGAffineTransformMakeRotation(self.angle); 690 | 691 | // position scroll view 692 | CGFloat width = fabs(cos(self.angle)) * self.cropView.frame.size.width + fabs(sin(self.angle)) * self.cropView.frame.size.height; 693 | CGFloat height = fabs(sin(self.angle)) * self.cropView.frame.size.width + fabs(cos(self.angle)) * self.cropView.frame.size.height; 694 | CGPoint center = self.scrollView.center; 695 | 696 | CGPoint contentOffset = self.scrollView.contentOffset; 697 | CGPoint contentOffsetCenter = CGPointMake(contentOffset.x + self.scrollView.bounds.size.width / 2, contentOffset.y + self.scrollView.bounds.size.height / 2); 698 | self.scrollView.bounds = CGRectMake(0, 0, width, height); 699 | CGPoint newContentOffset = CGPointMake(contentOffsetCenter.x - self.scrollView.bounds.size.width / 2, contentOffsetCenter.y - self.scrollView.bounds.size.height / 2); 700 | self.scrollView.contentOffset = newContentOffset; 701 | self.scrollView.center = center; 702 | 703 | // scale scroll view 704 | BOOL shouldScale = self.scrollView.contentSize.width / self.scrollView.bounds.size.width <= 1.0 || self.scrollView.contentSize.height / self.scrollView.bounds.size.height <= 1.0; 705 | if (!self.manualZoomed || shouldScale) { 706 | [self.scrollView setZoomScale:[self.scrollView zoomScaleToBound] animated:NO]; 707 | self.scrollView.minimumZoomScale = [self.scrollView zoomScaleToBound]; 708 | 709 | self.manualZoomed = NO; 710 | } 711 | 712 | [self checkScrollViewContentOffset]; 713 | } 714 | 715 | - (void)sliderTouchEnded:(id)sender 716 | { 717 | [self.cropView dismissGridLines]; 718 | } 719 | 720 | - (void)resetBtnTapped:(id)sender 721 | { 722 | [UIView animateWithDuration:0.25 animations:^{ 723 | self.angle = 0; 724 | 725 | self.scrollView.transform = CGAffineTransformIdentity; 726 | self.scrollView.center = CGPointMake(CGRectGetWidth(self.frame) / 2, self.centerY); 727 | self.scrollView.bounds = CGRectMake(0, 0, self.originalSize.width, self.originalSize.height); 728 | self.scrollView.minimumZoomScale = 1; 729 | [self.scrollView setZoomScale:1 animated:NO]; 730 | 731 | self.cropView.center = self.scrollView.center; 732 | [self updateMasks:NO]; 733 | 734 | [self.slider setValue:0 animated:YES]; 735 | }]; 736 | } 737 | 738 | - (CGPoint)photoTranslation 739 | { 740 | CGRect rect = [self.photoContentView convertRect:self.photoContentView.bounds toView:self]; 741 | CGPoint point = CGPointMake(rect.origin.x + rect.size.width / 2, rect.origin.y + rect.size.height / 2); 742 | CGPoint zeroPoint = CGPointMake(CGRectGetWidth(self.frame) / 2, self.centerY); 743 | return CGPointMake(point.x - zeroPoint.x, point.y - zeroPoint.y); 744 | } 745 | 746 | @end 747 | --------------------------------------------------------------------------------