├── screenshot.png ├── RKUserResizableViewDemo ├── RKUserResizableViewDemo │ ├── Assets.xcassets │ │ ├── Contents.json │ │ ├── smiley.imageset │ │ │ ├── smiley.jpg │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── Info.plist │ ├── Base.lproj │ │ ├── Main.storyboard │ │ └── LaunchScreen.storyboard │ ├── AppDelegate.swift │ ├── ViewController.swift │ └── RKUserResizableView.swift └── RKUserResizableViewDemo.xcodeproj │ ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcuserdata │ │ └── user.xcuserdatad │ │ └── UserInterfaceState.xcuserstate │ ├── xcuserdata │ └── user.xcuserdatad │ │ └── xcschemes │ │ ├── xcschememanagement.plist │ │ └── RKUserResizableViewDemo.xcscheme │ └── project.pbxproj ├── LICENSE ├── README.md └── RKUserResizableView.swift /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RajatJain4061/RKUserResizableView/HEAD/screenshot.png -------------------------------------------------------------------------------- /RKUserResizableViewDemo/RKUserResizableViewDemo/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /RKUserResizableViewDemo/RKUserResizableViewDemo/Assets.xcassets/smiley.imageset/smiley.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RajatJain4061/RKUserResizableView/HEAD/RKUserResizableViewDemo/RKUserResizableViewDemo/Assets.xcassets/smiley.imageset/smiley.jpg -------------------------------------------------------------------------------- /RKUserResizableViewDemo/RKUserResizableViewDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /RKUserResizableViewDemo/RKUserResizableViewDemo.xcodeproj/project.xcworkspace/xcuserdata/user.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RajatJain4061/RKUserResizableView/HEAD/RKUserResizableViewDemo/RKUserResizableViewDemo.xcodeproj/project.xcworkspace/xcuserdata/user.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /RKUserResizableViewDemo/RKUserResizableViewDemo/Assets.xcassets/smiley.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "smiley.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /RKUserResizableViewDemo/RKUserResizableViewDemo.xcodeproj/xcuserdata/user.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | RKUserResizableViewDemo.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | CEE4F5FC1F5675B300C12FF8 16 | 17 | primary 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Rajat jain 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 | -------------------------------------------------------------------------------- /RKUserResizableViewDemo/RKUserResizableViewDemo/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "ipad", 5 | "size" : "20x20", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "ipad", 10 | "size" : "20x20", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "ipad", 15 | "size" : "29x29", 16 | "scale" : "1x" 17 | }, 18 | { 19 | "idiom" : "ipad", 20 | "size" : "29x29", 21 | "scale" : "2x" 22 | }, 23 | { 24 | "idiom" : "ipad", 25 | "size" : "40x40", 26 | "scale" : "1x" 27 | }, 28 | { 29 | "idiom" : "ipad", 30 | "size" : "40x40", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "idiom" : "ipad", 35 | "size" : "76x76", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "ipad", 40 | "size" : "76x76", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "83.5x83.5", 46 | "scale" : "2x" 47 | } 48 | ], 49 | "info" : { 50 | "version" : 1, 51 | "author" : "xcode" 52 | } 53 | } -------------------------------------------------------------------------------- /RKUserResizableViewDemo/RKUserResizableViewDemo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations~ipad 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationPortraitUpsideDown 35 | UIInterfaceOrientationLandscapeLeft 36 | UIInterfaceOrientationLandscapeRight 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /RKUserResizableViewDemo/RKUserResizableViewDemo/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 | 27 | -------------------------------------------------------------------------------- /RKUserResizableViewDemo/RKUserResizableViewDemo/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /RKUserResizableViewDemo/RKUserResizableViewDemo/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // RKUserResizableViewDemo 4 | // 5 | // Created by user on 30/08/17. 6 | // Copyright © 2017 rk. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(_ application: UIApplication) { 23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 24 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(_ application: UIApplication) { 28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(_ application: UIApplication) { 33 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | func applicationDidBecomeActive(_ application: UIApplication) { 37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 38 | } 39 | 40 | func applicationWillTerminate(_ application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /RKUserResizableViewDemo/RKUserResizableViewDemo/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // RKUserResizableViewDemo 4 | // 5 | // Created by user on 30/08/17. 6 | // Copyright © 2017 rk. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ViewController: UIViewController { 12 | 13 | var currentlyEditingView:RKUserResizableView? = nil 14 | var lastEditedView:RKUserResizableView? = nil 15 | 16 | override func viewDidLoad() { 17 | super.viewDidLoad() 18 | 19 | // (1) Create a user resizable view with a simple red background content view. 20 | let gripFrame = CGRect(x: 50, y: 50, width: 200, height: 150) 21 | let userResizableView = RKUserResizableView(frame: gripFrame) 22 | userResizableView.delegate = self 23 | let contentView = UIView(frame: gripFrame) 24 | contentView.backgroundColor = UIColor.red 25 | userResizableView.contentView = contentView 26 | userResizableView.showEditingHandles() 27 | userResizableView.isPreventsPositionOutsideSuperview 28 | currentlyEditingView = userResizableView 29 | lastEditedView = userResizableView 30 | self.view.addSubview(userResizableView) 31 | 32 | // (2) Create a second resizable view with a UIImageView as the content. 33 | let imageFrame = CGRect(x: 50, y: 200, width: 200, height: 200) 34 | let imageResizableView = RKUserResizableView(frame: imageFrame) 35 | let imageView = UIImageView(image: UIImage(named: "smiley")) 36 | imageResizableView.contentView = imageView 37 | imageResizableView.delegate = self 38 | self.view.addSubview(imageResizableView) 39 | } 40 | 41 | override func didReceiveMemoryWarning() { 42 | super.didReceiveMemoryWarning() 43 | // Dispose of any resources that can be recreated. 44 | } 45 | 46 | 47 | } 48 | 49 | extension ViewController:RKUserResizableViewDelegate { 50 | func userResizableViewDidBeginEditing(_ userResizableView: RKUserResizableView) { 51 | currentlyEditingView?.hideEditingHandles() 52 | currentlyEditingView = userResizableView 53 | } 54 | 55 | func userResizableViewDidEndEditing(_ userResizableView: RKUserResizableView) { 56 | lastEditedView = userResizableView 57 | } 58 | } 59 | 60 | extension ViewController:UIGestureRecognizerDelegate { 61 | func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { 62 | if ((currentlyEditingView?.hitTest(touch.location(in: currentlyEditingView), with: nil)) != nil) { 63 | return false 64 | } 65 | return true 66 | } 67 | 68 | func hideEditingHandles() { 69 | // We only want the gesture recognizer to end the editing session on the last 70 | // edited view. We wouldn't want to dismiss an editing session in progress. 71 | lastEditedView?.hideEditingHandles() 72 | } 73 | } 74 | 75 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RKUserResizableView 2 | RKUserResizableView is a user-resizable, user-repositionable UIView subclass built for iOS (Swift 3). 3 | 4 | 5 | RKUserResizableView is a user-resizable, user-repositionable UIView subclass. It is modeled after the resizable image view from the Pages iOS app. Any UIView can be provided as the content view for the RKUserResizableView. 6 | 7 | Screenshot 8 | ---- 9 | ![RKUserResizableViewScreenshot](/screenshot.png?raw=true "RKUserResizableView") 10 | 11 | How To Use It 12 | ------------- 13 | 14 | ### Installation 15 | 16 | Include RKUserResizableView.swift in your project. 17 | 18 | ### Setting up the RKUserResizableView 19 | 20 | You'll need to construct a new instance of RKUserResizableView. Then, set the contentView on the RKUserResizableView to the view you'd like the user to interact with. 21 | 22 | ``` swift 23 | 24 | var currentlyEditingView:RKUserResizableView? = nil 25 | var lastEditedView:RKUserResizableView? = nil 26 | 27 | override func viewDidLoad() { 28 | super.viewDidLoad() 29 | 30 | // (1) Create a user resizable view with a simple red background content view. 31 | let gripFrame = CGRect(x: 50, y: 50, width: 200, height: 150) 32 | let userResizableView = RKUserResizableView(frame: gripFrame) 33 | userResizableView.delegate = self 34 | let contentView = UIView(frame: gripFrame) 35 | contentView.backgroundColor = UIColor.red 36 | userResizableView.contentView = contentView 37 | userResizableView.showEditingHandles() 38 | currentlyEditingView = userResizableView 39 | lastEditedView = userResizableView 40 | self.view.addSubview(userResizableView) 41 | 42 | // (2) Create a second resizable view with a UIImageView as the content. 43 | let imageFrame = CGRect(x: 50, y: 200, width: 200, height: 200) 44 | let imageResizableView = RKUserResizableView(frame: imageFrame) 45 | let imageView = UIImageView(image: UIImage(named: "smiley")) 46 | imageResizableView.contentView = imageView 47 | imageResizableView.delegate = self 48 | self.view.addSubview(imageResizableView) 49 | } 50 | ``` 51 | 52 | If you'd like to receive callbacks when the RKUserResizableView receives touchBegan:, touchesEnded: and touchesCancelled: messages, set the delegate on the RKUserResizableView accordingly. 53 | 54 | ``` swift 55 | userResizableView.delegate = self; 56 | ``` 57 | 58 | Then implement the following delegate methods. 59 | 60 | ``` swift 61 | // Called when the resizable view receives touchesBegan: and activates the editing handles. 62 | func userResizableViewDidBeginEditing(_ userResizableView: RKUserResizableView) 63 | 64 | // Called when the resizable view receives touchesEnded: or touchesCancelled: 65 | func userResizableViewDidEndEditing(_ userResizableView: RKUserResizableView) 66 | ``` 67 | 68 | By default, RKUserResizableView will show the editing handles (as seen in the screenshot above) whenever it receives a touch event. The editing handles will remain visible even after the userResizableViewDidEndEditing: message is sent. This is to provide visual feedback to the user that the view is indeed moveable and resizable. If you'd like to dismiss the editing handles, you must explicitly call hideEditingHandles(). 69 | 70 | The RKUserResizableView is customizable using the following properties: 71 | 72 | ``` swift 73 | // Default is 48.0 for each. 74 | var minWidth: CGFloat = 48.0 75 | var minHeight: CGFloat = 48.0 76 | 77 | // Defaults to YES. Disables the user from dragging the view outside the parent view's bounds. 78 | var isPreventsPositionOutsideSuperview: Bool = true 79 | ``` 80 | 81 | For an example of how to use RKUserResizableView, please see the included example project. 82 | 83 | ## Acknowledgments 84 | 85 | * Hat tip to Stephen Poletto [spoletto](https://github.com/spoletto) 86 | 87 | -------------------------------------------------------------------------------- /RKUserResizableViewDemo/RKUserResizableViewDemo.xcodeproj/xcuserdata/user.xcuserdatad/xcschemes/RKUserResizableViewDemo.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /RKUserResizableViewDemo/RKUserResizableViewDemo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | CEE4F6011F5675B300C12FF8 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE4F6001F5675B300C12FF8 /* AppDelegate.swift */; }; 11 | CEE4F6031F5675B300C12FF8 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE4F6021F5675B300C12FF8 /* ViewController.swift */; }; 12 | CEE4F6061F5675B300C12FF8 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = CEE4F6041F5675B300C12FF8 /* Main.storyboard */; }; 13 | CEE4F6081F5675B300C12FF8 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CEE4F6071F5675B300C12FF8 /* Assets.xcassets */; }; 14 | CEE4F60B1F5675B300C12FF8 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = CEE4F6091F5675B300C12FF8 /* LaunchScreen.storyboard */; }; 15 | CEE4F6131F5675E800C12FF8 /* RKUserResizableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE4F6121F5675E800C12FF8 /* RKUserResizableView.swift */; }; 16 | /* End PBXBuildFile section */ 17 | 18 | /* Begin PBXFileReference section */ 19 | CEE4F5FD1F5675B300C12FF8 /* RKUserResizableViewDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = RKUserResizableViewDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 20 | CEE4F6001F5675B300C12FF8 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 21 | CEE4F6021F5675B300C12FF8 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 22 | CEE4F6051F5675B300C12FF8 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 23 | CEE4F6071F5675B300C12FF8 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 24 | CEE4F60A1F5675B300C12FF8 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 25 | CEE4F60C1F5675B300C12FF8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 26 | CEE4F6121F5675E800C12FF8 /* RKUserResizableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RKUserResizableView.swift; sourceTree = ""; }; 27 | /* End PBXFileReference section */ 28 | 29 | /* Begin PBXFrameworksBuildPhase section */ 30 | CEE4F5FA1F5675B300C12FF8 /* Frameworks */ = { 31 | isa = PBXFrameworksBuildPhase; 32 | buildActionMask = 2147483647; 33 | files = ( 34 | ); 35 | runOnlyForDeploymentPostprocessing = 0; 36 | }; 37 | /* End PBXFrameworksBuildPhase section */ 38 | 39 | /* Begin PBXGroup section */ 40 | CEE4F5F41F5675B300C12FF8 = { 41 | isa = PBXGroup; 42 | children = ( 43 | CEE4F5FF1F5675B300C12FF8 /* RKUserResizableViewDemo */, 44 | CEE4F5FE1F5675B300C12FF8 /* Products */, 45 | ); 46 | sourceTree = ""; 47 | }; 48 | CEE4F5FE1F5675B300C12FF8 /* Products */ = { 49 | isa = PBXGroup; 50 | children = ( 51 | CEE4F5FD1F5675B300C12FF8 /* RKUserResizableViewDemo.app */, 52 | ); 53 | name = Products; 54 | sourceTree = ""; 55 | }; 56 | CEE4F5FF1F5675B300C12FF8 /* RKUserResizableViewDemo */ = { 57 | isa = PBXGroup; 58 | children = ( 59 | CEE4F6001F5675B300C12FF8 /* AppDelegate.swift */, 60 | CEE4F6021F5675B300C12FF8 /* ViewController.swift */, 61 | CEE4F6041F5675B300C12FF8 /* Main.storyboard */, 62 | CEE4F6071F5675B300C12FF8 /* Assets.xcassets */, 63 | CEE4F6091F5675B300C12FF8 /* LaunchScreen.storyboard */, 64 | CEE4F60C1F5675B300C12FF8 /* Info.plist */, 65 | CEE4F6121F5675E800C12FF8 /* RKUserResizableView.swift */, 66 | ); 67 | path = RKUserResizableViewDemo; 68 | sourceTree = ""; 69 | }; 70 | /* End PBXGroup section */ 71 | 72 | /* Begin PBXNativeTarget section */ 73 | CEE4F5FC1F5675B300C12FF8 /* RKUserResizableViewDemo */ = { 74 | isa = PBXNativeTarget; 75 | buildConfigurationList = CEE4F60F1F5675B300C12FF8 /* Build configuration list for PBXNativeTarget "RKUserResizableViewDemo" */; 76 | buildPhases = ( 77 | CEE4F5F91F5675B300C12FF8 /* Sources */, 78 | CEE4F5FA1F5675B300C12FF8 /* Frameworks */, 79 | CEE4F5FB1F5675B300C12FF8 /* Resources */, 80 | ); 81 | buildRules = ( 82 | ); 83 | dependencies = ( 84 | ); 85 | name = RKUserResizableViewDemo; 86 | productName = RKUserResizableViewDemo; 87 | productReference = CEE4F5FD1F5675B300C12FF8 /* RKUserResizableViewDemo.app */; 88 | productType = "com.apple.product-type.application"; 89 | }; 90 | /* End PBXNativeTarget section */ 91 | 92 | /* Begin PBXProject section */ 93 | CEE4F5F51F5675B300C12FF8 /* Project object */ = { 94 | isa = PBXProject; 95 | attributes = { 96 | LastSwiftUpdateCheck = 0830; 97 | LastUpgradeCheck = 0830; 98 | ORGANIZATIONNAME = rk; 99 | TargetAttributes = { 100 | CEE4F5FC1F5675B300C12FF8 = { 101 | CreatedOnToolsVersion = 8.3.2; 102 | DevelopmentTeam = 952ZU65CH6; 103 | ProvisioningStyle = Automatic; 104 | }; 105 | }; 106 | }; 107 | buildConfigurationList = CEE4F5F81F5675B300C12FF8 /* Build configuration list for PBXProject "RKUserResizableViewDemo" */; 108 | compatibilityVersion = "Xcode 3.2"; 109 | developmentRegion = English; 110 | hasScannedForEncodings = 0; 111 | knownRegions = ( 112 | en, 113 | Base, 114 | ); 115 | mainGroup = CEE4F5F41F5675B300C12FF8; 116 | productRefGroup = CEE4F5FE1F5675B300C12FF8 /* Products */; 117 | projectDirPath = ""; 118 | projectRoot = ""; 119 | targets = ( 120 | CEE4F5FC1F5675B300C12FF8 /* RKUserResizableViewDemo */, 121 | ); 122 | }; 123 | /* End PBXProject section */ 124 | 125 | /* Begin PBXResourcesBuildPhase section */ 126 | CEE4F5FB1F5675B300C12FF8 /* Resources */ = { 127 | isa = PBXResourcesBuildPhase; 128 | buildActionMask = 2147483647; 129 | files = ( 130 | CEE4F60B1F5675B300C12FF8 /* LaunchScreen.storyboard in Resources */, 131 | CEE4F6081F5675B300C12FF8 /* Assets.xcassets in Resources */, 132 | CEE4F6061F5675B300C12FF8 /* Main.storyboard in Resources */, 133 | ); 134 | runOnlyForDeploymentPostprocessing = 0; 135 | }; 136 | /* End PBXResourcesBuildPhase section */ 137 | 138 | /* Begin PBXSourcesBuildPhase section */ 139 | CEE4F5F91F5675B300C12FF8 /* Sources */ = { 140 | isa = PBXSourcesBuildPhase; 141 | buildActionMask = 2147483647; 142 | files = ( 143 | CEE4F6031F5675B300C12FF8 /* ViewController.swift in Sources */, 144 | CEE4F6131F5675E800C12FF8 /* RKUserResizableView.swift in Sources */, 145 | CEE4F6011F5675B300C12FF8 /* AppDelegate.swift in Sources */, 146 | ); 147 | runOnlyForDeploymentPostprocessing = 0; 148 | }; 149 | /* End PBXSourcesBuildPhase section */ 150 | 151 | /* Begin PBXVariantGroup section */ 152 | CEE4F6041F5675B300C12FF8 /* Main.storyboard */ = { 153 | isa = PBXVariantGroup; 154 | children = ( 155 | CEE4F6051F5675B300C12FF8 /* Base */, 156 | ); 157 | name = Main.storyboard; 158 | sourceTree = ""; 159 | }; 160 | CEE4F6091F5675B300C12FF8 /* LaunchScreen.storyboard */ = { 161 | isa = PBXVariantGroup; 162 | children = ( 163 | CEE4F60A1F5675B300C12FF8 /* Base */, 164 | ); 165 | name = LaunchScreen.storyboard; 166 | sourceTree = ""; 167 | }; 168 | /* End PBXVariantGroup section */ 169 | 170 | /* Begin XCBuildConfiguration section */ 171 | CEE4F60D1F5675B300C12FF8 /* Debug */ = { 172 | isa = XCBuildConfiguration; 173 | buildSettings = { 174 | ALWAYS_SEARCH_USER_PATHS = NO; 175 | CLANG_ANALYZER_NONNULL = YES; 176 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 177 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 178 | CLANG_CXX_LIBRARY = "libc++"; 179 | CLANG_ENABLE_MODULES = YES; 180 | CLANG_ENABLE_OBJC_ARC = YES; 181 | CLANG_WARN_BOOL_CONVERSION = YES; 182 | CLANG_WARN_CONSTANT_CONVERSION = YES; 183 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 184 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 185 | CLANG_WARN_EMPTY_BODY = YES; 186 | CLANG_WARN_ENUM_CONVERSION = YES; 187 | CLANG_WARN_INFINITE_RECURSION = YES; 188 | CLANG_WARN_INT_CONVERSION = YES; 189 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 190 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 191 | CLANG_WARN_UNREACHABLE_CODE = YES; 192 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 193 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 194 | COPY_PHASE_STRIP = NO; 195 | DEBUG_INFORMATION_FORMAT = dwarf; 196 | ENABLE_STRICT_OBJC_MSGSEND = YES; 197 | ENABLE_TESTABILITY = YES; 198 | GCC_C_LANGUAGE_STANDARD = gnu99; 199 | GCC_DYNAMIC_NO_PIC = NO; 200 | GCC_NO_COMMON_BLOCKS = YES; 201 | GCC_OPTIMIZATION_LEVEL = 0; 202 | GCC_PREPROCESSOR_DEFINITIONS = ( 203 | "DEBUG=1", 204 | "$(inherited)", 205 | ); 206 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 207 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 208 | GCC_WARN_UNDECLARED_SELECTOR = YES; 209 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 210 | GCC_WARN_UNUSED_FUNCTION = YES; 211 | GCC_WARN_UNUSED_VARIABLE = YES; 212 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 213 | MTL_ENABLE_DEBUG_INFO = YES; 214 | ONLY_ACTIVE_ARCH = YES; 215 | SDKROOT = iphoneos; 216 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 217 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 218 | TARGETED_DEVICE_FAMILY = 2; 219 | }; 220 | name = Debug; 221 | }; 222 | CEE4F60E1F5675B300C12FF8 /* Release */ = { 223 | isa = XCBuildConfiguration; 224 | buildSettings = { 225 | ALWAYS_SEARCH_USER_PATHS = NO; 226 | CLANG_ANALYZER_NONNULL = YES; 227 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 228 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 229 | CLANG_CXX_LIBRARY = "libc++"; 230 | CLANG_ENABLE_MODULES = YES; 231 | CLANG_ENABLE_OBJC_ARC = YES; 232 | CLANG_WARN_BOOL_CONVERSION = YES; 233 | CLANG_WARN_CONSTANT_CONVERSION = YES; 234 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 235 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 236 | CLANG_WARN_EMPTY_BODY = YES; 237 | CLANG_WARN_ENUM_CONVERSION = YES; 238 | CLANG_WARN_INFINITE_RECURSION = YES; 239 | CLANG_WARN_INT_CONVERSION = YES; 240 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 241 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 242 | CLANG_WARN_UNREACHABLE_CODE = YES; 243 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 244 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 245 | COPY_PHASE_STRIP = NO; 246 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 247 | ENABLE_NS_ASSERTIONS = NO; 248 | ENABLE_STRICT_OBJC_MSGSEND = YES; 249 | GCC_C_LANGUAGE_STANDARD = gnu99; 250 | GCC_NO_COMMON_BLOCKS = YES; 251 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 252 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 253 | GCC_WARN_UNDECLARED_SELECTOR = YES; 254 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 255 | GCC_WARN_UNUSED_FUNCTION = YES; 256 | GCC_WARN_UNUSED_VARIABLE = YES; 257 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 258 | MTL_ENABLE_DEBUG_INFO = NO; 259 | SDKROOT = iphoneos; 260 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 261 | TARGETED_DEVICE_FAMILY = 2; 262 | VALIDATE_PRODUCT = YES; 263 | }; 264 | name = Release; 265 | }; 266 | CEE4F6101F5675B300C12FF8 /* Debug */ = { 267 | isa = XCBuildConfiguration; 268 | buildSettings = { 269 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 270 | DEVELOPMENT_TEAM = 952ZU65CH6; 271 | INFOPLIST_FILE = RKUserResizableViewDemo/Info.plist; 272 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 273 | PRODUCT_BUNDLE_IDENTIFIER = com.rajatjain4061.RKUserResizableViewDemo; 274 | PRODUCT_NAME = "$(TARGET_NAME)"; 275 | SWIFT_VERSION = 3.0; 276 | }; 277 | name = Debug; 278 | }; 279 | CEE4F6111F5675B300C12FF8 /* Release */ = { 280 | isa = XCBuildConfiguration; 281 | buildSettings = { 282 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 283 | DEVELOPMENT_TEAM = 952ZU65CH6; 284 | INFOPLIST_FILE = RKUserResizableViewDemo/Info.plist; 285 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 286 | PRODUCT_BUNDLE_IDENTIFIER = com.rajatjain4061.RKUserResizableViewDemo; 287 | PRODUCT_NAME = "$(TARGET_NAME)"; 288 | SWIFT_VERSION = 3.0; 289 | }; 290 | name = Release; 291 | }; 292 | /* End XCBuildConfiguration section */ 293 | 294 | /* Begin XCConfigurationList section */ 295 | CEE4F5F81F5675B300C12FF8 /* Build configuration list for PBXProject "RKUserResizableViewDemo" */ = { 296 | isa = XCConfigurationList; 297 | buildConfigurations = ( 298 | CEE4F60D1F5675B300C12FF8 /* Debug */, 299 | CEE4F60E1F5675B300C12FF8 /* Release */, 300 | ); 301 | defaultConfigurationIsVisible = 0; 302 | defaultConfigurationName = Release; 303 | }; 304 | CEE4F60F1F5675B300C12FF8 /* Build configuration list for PBXNativeTarget "RKUserResizableViewDemo" */ = { 305 | isa = XCConfigurationList; 306 | buildConfigurations = ( 307 | CEE4F6101F5675B300C12FF8 /* Debug */, 308 | CEE4F6111F5675B300C12FF8 /* Release */, 309 | ); 310 | defaultConfigurationIsVisible = 0; 311 | }; 312 | /* End XCConfigurationList section */ 313 | }; 314 | rootObject = CEE4F5F51F5675B300C12FF8 /* Project object */; 315 | } 316 | -------------------------------------------------------------------------------- /RKUserResizableView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RKUserResizableView.swift 3 | // RKUserResizableViewDemo 4 | // 5 | // Created by Rajat Jain on 28/08/17. 6 | // Converted to Swift from https://github.com/spoletto/SPUserResizableView 7 | // 8 | 9 | 10 | import UIKit 11 | 12 | struct RKUserResizableViewAnchorPoint { 13 | var adjustsX: CGFloat = 0.0 14 | var adjustsY: CGFloat = 0.0 15 | var adjustsH: CGFloat = 0.0 16 | var adjustsW: CGFloat = 0.0 17 | } 18 | 19 | struct RKUserResizableViewAnchorPointPair { 20 | var point: CGPoint = CGPoint.zero 21 | var anchorPoint: RKUserResizableViewAnchorPoint = RKUserResizableViewAnchorPoint() 22 | } 23 | 24 | protocol RKUserResizableViewDelegate: NSObjectProtocol { 25 | // Called when the resizable view receives touchesBegan: and activates the editing handles. 26 | func userResizableViewDidBeginEditing(_ userResizableView: RKUserResizableView) 27 | 28 | // Called when the resizable view receives touchesEnded: or touchesCancelled: 29 | func userResizableViewDidEndEditing(_ userResizableView: RKUserResizableView) 30 | } 31 | 32 | 33 | class RKUserResizableView:UIView { 34 | 35 | var borderView: GripViewBorderView? 36 | 37 | // Will be retained as a subview. 38 | var _contentView: UIView? 39 | 40 | var contentView:UIView? { 41 | set (newValue) { 42 | newValue?.removeFromSuperview() 43 | newValue?.frame = bounds.insetBy(dx: RKUserResizableViewGlobalInset + RKUserResizableViewInteractiveBorderSize / 2, dy: RKUserResizableViewGlobalInset + RKUserResizableViewInteractiveBorderSize / 2) 44 | addSubview(newValue!) 45 | // Ensure the border view is always on top by removing it and adding it to the end of the subview list. 46 | borderView?.removeFromSuperview() 47 | addSubview(borderView!) 48 | self._contentView = newValue 49 | } 50 | get { 51 | return self._contentView 52 | } 53 | } 54 | 55 | var touchStart = CGPoint.zero 56 | 57 | // Default is 48.0 for each. 58 | var minWidth: CGFloat = 48.0 59 | var minHeight: CGFloat = 48.0 60 | 61 | // Used to determine which components of the bounds we'll be modifying, based upon where the user's touch started. 62 | var anchorPoint = RKUserResizableViewAnchorPoint() 63 | 64 | weak var delegate: RKUserResizableViewDelegate? 65 | 66 | /* Let's inset everything that's drawn (the handles and the content view) 67 | so that users can trigger a resize from a few pixels outside of 68 | what they actually see as the bounding box. */ 69 | let RKUserResizableViewGlobalInset:CGFloat = 5.0 70 | let RKUserResizableViewDefaultMinWidth:CGFloat = 48.0 71 | let RKUserResizableViewDefaultMinHeight:CGFloat = 48.0 72 | let RKUserResizableViewInteractiveBorderSize:CGFloat = 10.0 73 | 74 | // Defaults to YES. Disables the user from dragging the view outside the parent view's bounds. 75 | 76 | var isPreventsPositionOutsideSuperview: Bool = true 77 | 78 | private var RKUserResizableViewNoResizeAnchorPoint = RKUserResizableViewAnchorPoint(adjustsX: 0.0, adjustsY: 0.0, adjustsH: 0.0, adjustsW: 0.0) 79 | private var RKUserResizableViewUpperLeftAnchorPoint = RKUserResizableViewAnchorPoint(adjustsX: 1.0, adjustsY: 1.0, adjustsH: -1.0, adjustsW: 1.0) 80 | private var RKUserResizableViewMiddleLeftAnchorPoint = RKUserResizableViewAnchorPoint(adjustsX: 1.0, adjustsY: 0.0, adjustsH: 0.0, adjustsW: 1.0) 81 | private var RKUserResizableViewLowerLeftAnchorPoint = RKUserResizableViewAnchorPoint(adjustsX: 1.0, adjustsY: 0.0, adjustsH: 1.0, adjustsW: 1.0) 82 | private var RKUserResizableViewUpperMiddleAnchorPoint = RKUserResizableViewAnchorPoint(adjustsX: 0.0, adjustsY: 1.0, adjustsH: -1.0, adjustsW: 0.0) 83 | private var RKUserResizableViewUpperRightAnchorPoint = RKUserResizableViewAnchorPoint(adjustsX: 0.0, adjustsY: 1.0, adjustsH: -1.0, adjustsW: -1.0) 84 | private var RKUserResizableViewMiddleRightAnchorPoint = RKUserResizableViewAnchorPoint(adjustsX: 0.0, adjustsY: 0.0, adjustsH: 0.0, adjustsW: -1.0) 85 | private var RKUserResizableViewLowerRightAnchorPoint = RKUserResizableViewAnchorPoint(adjustsX: 0.0, adjustsY: 0.0, adjustsH: 1.0, adjustsW: -1.0) 86 | private var RKUserResizableViewLowerMiddleAnchorPoint = RKUserResizableViewAnchorPoint(adjustsX: 0.0, adjustsY: 0.0, adjustsH: 1.0, adjustsW: 0.0) 87 | 88 | func setupDefaultAttributes() { 89 | borderView = GripViewBorderView(frame: bounds.insetBy(dx: CGFloat(RKUserResizableViewGlobalInset), dy: CGFloat(RKUserResizableViewGlobalInset))) 90 | borderView?.isHidden = true 91 | self.addSubview(borderView!) 92 | minWidth = RKUserResizableViewDefaultMinWidth 93 | minHeight = RKUserResizableViewDefaultMinHeight 94 | isPreventsPositionOutsideSuperview = true 95 | } 96 | 97 | override init(frame: CGRect) { 98 | super.init(frame: frame) 99 | setupDefaultAttributes() 100 | } 101 | 102 | required init?(coder aDecoder: NSCoder) { 103 | super.init(coder: aDecoder) 104 | setupDefaultAttributes() 105 | } 106 | 107 | func setViewFrame(_ newFrame: CGRect) { 108 | self.frame = newFrame 109 | contentView?.frame = bounds.insetBy(dx: RKUserResizableViewGlobalInset + RKUserResizableViewInteractiveBorderSize / 2, dy: RKUserResizableViewGlobalInset + RKUserResizableViewInteractiveBorderSize / 2) 110 | borderView?.frame = bounds.insetBy(dx: RKUserResizableViewGlobalInset, dy: RKUserResizableViewGlobalInset) 111 | borderView?.setNeedsDisplay() 112 | 113 | } 114 | 115 | private func distanceBetweenTwoPoints(point1: CGPoint, point2: CGPoint) -> CGFloat { 116 | let dx: CGFloat = point2.x - point1.x 117 | let dy: CGFloat = point2.y - point1.y 118 | return sqrt(dx * dx + dy * dy) 119 | } 120 | 121 | func anchorPoint(forTouchLocation touchPoint: CGPoint) -> RKUserResizableViewAnchorPoint { 122 | // (1) Calculate the positions of each of the anchor points. 123 | let upperLeft = RKUserResizableViewAnchorPointPair(point: CGPoint(x: 0.0, y: 0.0), anchorPoint: RKUserResizableViewUpperLeftAnchorPoint) 124 | 125 | let upperMiddle = RKUserResizableViewAnchorPointPair(point: CGPoint(x: bounds.size.width / 2, y: 0.0), anchorPoint: RKUserResizableViewUpperMiddleAnchorPoint) 126 | 127 | let upperRight = RKUserResizableViewAnchorPointPair(point: CGPoint(x: bounds.size.width, y: 0.0), anchorPoint: RKUserResizableViewUpperRightAnchorPoint) 128 | 129 | let middleRight = RKUserResizableViewAnchorPointPair(point: CGPoint(x: bounds.size.width, y: bounds.size.height / 2), anchorPoint: RKUserResizableViewMiddleRightAnchorPoint) 130 | 131 | let lowerRight = RKUserResizableViewAnchorPointPair(point: CGPoint(x: bounds.size.width, y: bounds.size.height), anchorPoint: RKUserResizableViewLowerRightAnchorPoint) 132 | 133 | let lowerMiddle = RKUserResizableViewAnchorPointPair(point: CGPoint(x: bounds.size.width / 2, y: bounds.size.height), anchorPoint: RKUserResizableViewLowerMiddleAnchorPoint) 134 | 135 | let lowerLeft = RKUserResizableViewAnchorPointPair(point: CGPoint(x: 0, y: bounds.size.height), anchorPoint: RKUserResizableViewLowerLeftAnchorPoint) 136 | 137 | let middleLeft = RKUserResizableViewAnchorPointPair(point: CGPoint(x: 0, y: bounds.size.height / 2), anchorPoint: RKUserResizableViewMiddleLeftAnchorPoint) 138 | 139 | let centerPoint = RKUserResizableViewAnchorPointPair(point: CGPoint(x: bounds.size.width / 2, y: bounds.size.height / 2), anchorPoint: RKUserResizableViewNoResizeAnchorPoint) 140 | 141 | // (2) Iterate over each of the anchor points and find the one closest to the user's touch. 142 | let allPoints: [RKUserResizableViewAnchorPointPair] = [upperLeft, upperRight, lowerRight, lowerLeft, upperMiddle, lowerMiddle, middleLeft, middleRight, centerPoint] 143 | var smallestDistance: CGFloat = CGFloat(MAXFLOAT) 144 | var closestPoint: RKUserResizableViewAnchorPointPair = centerPoint 145 | for i in 0..<9 { 146 | let distance: CGFloat = distanceBetweenTwoPoints(point1: touchPoint, point2: allPoints[i].point) 147 | if distance < smallestDistance { 148 | closestPoint = allPoints[i] 149 | smallestDistance = distance 150 | } 151 | } 152 | return closestPoint.anchorPoint 153 | } 154 | 155 | func isResizing() -> Bool { 156 | return (anchorPoint.adjustsH != 0 || anchorPoint.adjustsW != 0 || anchorPoint.adjustsX != 0 || anchorPoint.adjustsY != 0) 157 | } 158 | 159 | override func touchesBegan(_ touches: Set, with event: UIEvent?) { 160 | super.touchesBegan(touches, with: event) 161 | // Notify the delegate we've begun our editing session. 162 | // if delegate?.responds(to: #selector(self.userResizableViewDidBeginEditing)) { 163 | delegate?.userResizableViewDidBeginEditing(self) 164 | // } 165 | borderView?.isHidden = false 166 | 167 | if let touch = touches.first { 168 | anchorPoint = anchorPoint(forTouchLocation: touch.location(in: self)) 169 | print(anchorPoint) 170 | // When resizing, all calculations are done in the superview's coordinate space. 171 | touchStart = touch.location(in: superview) 172 | if !isResizing() { 173 | // When translating, all calculations are done in the view's coordinate space. 174 | touchStart = touch.location(in: self) 175 | } 176 | } 177 | } 178 | 179 | override func touchesEnded(_ touches: Set, with event: UIEvent?) { 180 | // Notify the delegate we've ended our editing session. 181 | delegate?.userResizableViewDidEndEditing(self) 182 | } 183 | 184 | override func touchesCancelled(_ touches: Set, with event: UIEvent?) { 185 | // Notify the delegate we've ended our editing session. 186 | delegate?.userResizableViewDidEndEditing(self) 187 | } 188 | 189 | func showEditingHandles() { 190 | borderView?.isHidden = false 191 | } 192 | 193 | func hideEditingHandles() { 194 | borderView?.isHidden = true 195 | } 196 | 197 | func resize(usingTouchLocation touchPoint: CGPoint) { 198 | // (1) Update the touch point if we're outside the superview. 199 | /* disabled by rajat jain on 2017-08-28 200 | if isPreventsPositionOutsideSuperview { 201 | let border: CGFloat = RKUserResizableViewGlobalInset + RKUserResizableViewInteractiveBorderSize / 2 202 | if touchPoint.x < border { 203 | touchPoint.x = border 204 | } 205 | if touchPoint.x > superview?.bounds.size.width - border { 206 | touchPoint.x = superview?.bounds.size.width - border 207 | } 208 | if touchPoint.y < border { 209 | touchPoint.y = border 210 | } 211 | if touchPoint.y > superview?.bounds.size.height - border { 212 | touchPoint.y = superview?.bounds.size.height - border 213 | } 214 | } 215 | */ 216 | // (2) Calculate the deltas using the current anchor point. 217 | var deltaW: CGFloat = anchorPoint.adjustsW * (touchStart.x - touchPoint.x) 218 | let deltaX: CGFloat = anchorPoint.adjustsX * (-1.0 * deltaW) 219 | var deltaH: CGFloat = anchorPoint.adjustsH * (touchPoint.y - touchStart.y) 220 | let deltaY: CGFloat = anchorPoint.adjustsY * (-1.0 * deltaH) 221 | // (3) Calculate the new frame. 222 | var newX: CGFloat = frame.origin.x + deltaX 223 | var newY: CGFloat = frame.origin.y + deltaY 224 | var newWidth: CGFloat = frame.size.width + deltaW 225 | var newHeight: CGFloat = frame.size.height + deltaH 226 | // (4) If the new frame is too small, cancel the changes. 227 | if newWidth < minWidth { 228 | newWidth = frame.size.width 229 | newX = frame.origin.x 230 | } 231 | if newHeight < minHeight { 232 | newHeight = frame.size.height 233 | newY = frame.origin.y 234 | } 235 | 236 | // (5) Ensure the resize won't cause the view to move offscreen. 237 | if isPreventsPositionOutsideSuperview { 238 | if let superView = self.superview { 239 | if newX < superView.bounds.origin.x { 240 | // Calculate how much to grow the width by such that the new X coordintae will align with the superview. 241 | deltaW = self.frame.origin.x - superView.bounds.origin.x 242 | newWidth = self.frame.size.width + deltaW 243 | newX = superView.bounds.origin.x 244 | } 245 | 246 | if newX + newWidth > superView.bounds.origin.x + superView.bounds.size.width { 247 | newWidth = superView.bounds.size.width - newX 248 | } 249 | 250 | if newY < superView.bounds.origin.y { 251 | // Calculate how much to grow the height by such that the new Y coordintae will align with the superview. 252 | deltaH = self.frame.origin.y - superView.bounds.origin.y 253 | newHeight = self.frame.size.height + deltaH 254 | newY = superView.bounds.origin.y 255 | } 256 | 257 | if newY + newHeight > superView.bounds.origin.y + superView.bounds.size.height { 258 | newHeight = superView.bounds.size.height - newY 259 | } 260 | 261 | } 262 | } 263 | 264 | self.setViewFrame(CGRect(x: newX, y: newY, width: newWidth, height: newHeight)) 265 | 266 | touchStart = touchPoint 267 | } 268 | 269 | func translate(usingTouchLocation touchPoint: CGPoint) { 270 | var newCenter = CGPoint(x: center.x + touchPoint.x - touchStart.x, y: center.y + touchPoint.y - touchStart.y) 271 | if isPreventsPositionOutsideSuperview { 272 | if let superView = self.superview { 273 | 274 | // Ensure the translation won't cause the view to move offscreen. 275 | let midPointX: CGFloat = bounds.midX 276 | if newCenter.x > superView.bounds.size.width - midPointX { 277 | newCenter.x = superView.bounds.size.width - midPointX 278 | } 279 | if newCenter.x < midPointX { 280 | newCenter.x = midPointX 281 | } 282 | let midPointY: CGFloat = bounds.midY 283 | if newCenter.y > superView.bounds.size.height - midPointY { 284 | newCenter.y = superView.bounds.size.height - midPointY 285 | } 286 | if newCenter.y < midPointY { 287 | newCenter.y = midPointY 288 | } 289 | } 290 | } 291 | center = newCenter 292 | } 293 | 294 | override func touchesMoved(_ touches: Set, with event: UIEvent?) { 295 | if isResizing() { 296 | if let superView = self.superview { 297 | resize(usingTouchLocation: (touches.first?.location(in: superView))!) 298 | } 299 | } 300 | else { 301 | translate(usingTouchLocation: (touches.first?.location(in: self))!) 302 | } 303 | } 304 | 305 | deinit { 306 | contentView?.removeFromSuperview() 307 | } 308 | } 309 | 310 | 311 | class GripViewBorderView: UIView { 312 | 313 | let RKUserResizableViewGlobalInset:CGFloat = 5.0 314 | let RKUserResizableViewDefaultMinWidth:CGFloat = 48.0 315 | let RKUserResizableViewDefaultMinHeight:CGFloat = 48.0 316 | let RKUserResizableViewInteractiveBorderSize:CGFloat = 10.0 317 | 318 | weak var delegate: RKUserResizableViewDelegate? 319 | // Will be retained as a subview. 320 | var contentView: UIView? 321 | // Default is 48.0 for each. 322 | var minWidth: CGFloat = 0.0 323 | var minHeight: CGFloat = 0.0 324 | // Defaults to YES. Disables the user from dragging the view outside the parent view's bounds. 325 | var isPreventsPositionOutsideSuperview: Bool = false 326 | 327 | var borderView: GripViewBorderView? 328 | var touchStart = CGPoint.zero 329 | // Used to determine which components of the bounds we'll be modifying, based upon where the user's touch started. 330 | var anchorPoint = RKUserResizableViewAnchorPoint() 331 | 332 | func hideEditingHandles() { 333 | } 334 | 335 | func showEditingHandles() { 336 | } 337 | 338 | override init(frame: CGRect) { 339 | // Clear background to ensure the content view shows through. 340 | super.init(frame: frame) 341 | self.backgroundColor = .clear 342 | } 343 | 344 | required init?(coder aDecoder: NSCoder) { 345 | fatalError("init(coder:) has not been implemented") 346 | } 347 | 348 | override func draw(_ rect: CGRect) { 349 | let context: CGContext? = UIGraphicsGetCurrentContext() 350 | context?.saveGState() 351 | 352 | // (1) Draw the bounding box. 353 | context?.setLineWidth(1.0) 354 | context?.setStrokeColor(UIColor.blue.cgColor) 355 | context?.addRect(bounds.insetBy(dx: RKUserResizableViewInteractiveBorderSize / 2, dy: RKUserResizableViewInteractiveBorderSize / 2)) 356 | context?.strokePath() 357 | 358 | // (2) Calculate the bounding boxes for each of the anchor points. 359 | let upperLeft = CGRect(x: 0.0, y: 0.0, width: RKUserResizableViewInteractiveBorderSize, height: RKUserResizableViewInteractiveBorderSize) 360 | let upperRight = CGRect(x: bounds.size.width - RKUserResizableViewInteractiveBorderSize, y: 0.0, width: RKUserResizableViewInteractiveBorderSize, height: RKUserResizableViewInteractiveBorderSize) 361 | let lowerRight = CGRect(x: bounds.size.width - RKUserResizableViewInteractiveBorderSize, y: bounds.size.height - RKUserResizableViewInteractiveBorderSize, width: RKUserResizableViewInteractiveBorderSize, height: RKUserResizableViewInteractiveBorderSize) 362 | let lowerLeft = CGRect(x: 0.0, y: bounds.size.height - RKUserResizableViewInteractiveBorderSize, width: RKUserResizableViewInteractiveBorderSize, height: RKUserResizableViewInteractiveBorderSize) 363 | let upperMiddle = CGRect(x: (bounds.size.width - RKUserResizableViewInteractiveBorderSize) / 2, y: 0.0, width: RKUserResizableViewInteractiveBorderSize, height: RKUserResizableViewInteractiveBorderSize) 364 | let lowerMiddle = CGRect(x: (bounds.size.width - RKUserResizableViewInteractiveBorderSize) / 2, y: bounds.size.height - RKUserResizableViewInteractiveBorderSize, width: RKUserResizableViewInteractiveBorderSize, height: RKUserResizableViewInteractiveBorderSize) 365 | let middleLeft = CGRect(x: 0.0, y: (bounds.size.height - RKUserResizableViewInteractiveBorderSize) / 2, width: RKUserResizableViewInteractiveBorderSize, height: RKUserResizableViewInteractiveBorderSize) 366 | let middleRight = CGRect(x: bounds.size.width - RKUserResizableViewInteractiveBorderSize, y: (bounds.size.height - RKUserResizableViewInteractiveBorderSize) / 2, width: RKUserResizableViewInteractiveBorderSize, height: RKUserResizableViewInteractiveBorderSize) 367 | 368 | // (3) Create the gradient to paint the anchor points. 369 | let colors: [CGFloat] = [0.4, 0.8, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0] 370 | let baseSpace: CGColorSpace? = CGColorSpaceCreateDeviceRGB() 371 | let gradient: CGGradient = CGGradient(colorSpace: baseSpace!, colorComponents: colors, locations: nil, count: 2)! 372 | 373 | // (4) Set up the stroke for drawing the border of each of the anchor points. 374 | context?.setLineWidth(2) 375 | context!.setShadow(offset: CGSize(width: 0.5, height: 0.5), blur: 1) 376 | context?.setStrokeColor(UIColor.white.cgColor) 377 | 378 | // (5) Fill each anchor point using the gradient, then stroke the border. 379 | let allPoints: [CGRect] = [upperLeft, upperRight, lowerRight, lowerLeft, upperMiddle, lowerMiddle, middleLeft, middleRight] 380 | for i in 0..<8 { 381 | let currPoint: CGRect = allPoints[i] 382 | context?.saveGState() 383 | //context?.addEllipse(in: currPoint) 384 | let currentPoint = CGRect(x: currPoint.origin.x, y: currPoint.origin.y, width: 10, height: 10); 385 | context?.addEllipse(in: currentPoint) 386 | context?.clip() 387 | let startPoint = CGPoint(x: currentPoint.midX, y: currentPoint.minY) 388 | let endPoint = CGPoint(x: currentPoint.midX, y: currentPoint.maxY) 389 | context?.drawLinearGradient(gradient, start: startPoint, end: endPoint, options: []) 390 | context?.restoreGState() 391 | context?.strokeEllipse(in: currentPoint.insetBy(dx: 1, dy: 1)) 392 | } 393 | context?.restoreGState() 394 | } 395 | } 396 | 397 | -------------------------------------------------------------------------------- /RKUserResizableViewDemo/RKUserResizableViewDemo/RKUserResizableView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RKUserResizableView.swift 3 | // RKUserResizableViewDemo 4 | // 5 | // Created by Rajat Jain on 28/08/17. 6 | // Converted to Swift from https://github.com/spoletto/SPUserResizableView 7 | // 8 | 9 | 10 | import UIKit 11 | 12 | struct RKUserResizableViewAnchorPoint { 13 | var adjustsX: CGFloat = 0.0 14 | var adjustsY: CGFloat = 0.0 15 | var adjustsH: CGFloat = 0.0 16 | var adjustsW: CGFloat = 0.0 17 | } 18 | 19 | struct RKUserResizableViewAnchorPointPair { 20 | var point: CGPoint = CGPoint.zero 21 | var anchorPoint: RKUserResizableViewAnchorPoint = RKUserResizableViewAnchorPoint() 22 | } 23 | 24 | protocol RKUserResizableViewDelegate: NSObjectProtocol { 25 | // Called when the resizable view receives touchesBegan: and activates the editing handles. 26 | func userResizableViewDidBeginEditing(_ userResizableView: RKUserResizableView) 27 | 28 | // Called when the resizable view receives touchesEnded: or touchesCancelled: 29 | func userResizableViewDidEndEditing(_ userResizableView: RKUserResizableView) 30 | } 31 | 32 | 33 | class RKUserResizableView:UIView { 34 | 35 | var borderView: GripViewBorderView? 36 | 37 | // Will be retained as a subview. 38 | var _contentView: UIView? 39 | 40 | var contentView:UIView? { 41 | set (newValue) { 42 | newValue?.removeFromSuperview() 43 | newValue?.frame = bounds.insetBy(dx: RKUserResizableViewGlobalInset + RKUserResizableViewInteractiveBorderSize / 2, dy: RKUserResizableViewGlobalInset + RKUserResizableViewInteractiveBorderSize / 2) 44 | addSubview(newValue!) 45 | // Ensure the border view is always on top by removing it and adding it to the end of the subview list. 46 | borderView?.removeFromSuperview() 47 | addSubview(borderView!) 48 | self._contentView = newValue 49 | } 50 | get { 51 | return self._contentView 52 | } 53 | } 54 | 55 | var touchStart = CGPoint.zero 56 | 57 | // Default is 48.0 for each. 58 | var minWidth: CGFloat = 48.0 59 | var minHeight: CGFloat = 48.0 60 | 61 | // Used to determine which components of the bounds we'll be modifying, based upon where the user's touch started. 62 | var anchorPoint = RKUserResizableViewAnchorPoint() 63 | 64 | weak var delegate: RKUserResizableViewDelegate? 65 | 66 | /* Let's inset everything that's drawn (the handles and the content view) 67 | so that users can trigger a resize from a few pixels outside of 68 | what they actually see as the bounding box. */ 69 | let RKUserResizableViewGlobalInset:CGFloat = 5.0 70 | let RKUserResizableViewDefaultMinWidth:CGFloat = 48.0 71 | let RKUserResizableViewDefaultMinHeight:CGFloat = 48.0 72 | let RKUserResizableViewInteractiveBorderSize:CGFloat = 10.0 73 | 74 | // Defaults to YES. Disables the user from dragging the view outside the parent view's bounds. 75 | 76 | var isPreventsPositionOutsideSuperview: Bool = true 77 | 78 | private var RKUserResizableViewNoResizeAnchorPoint = RKUserResizableViewAnchorPoint(adjustsX: 0.0, adjustsY: 0.0, adjustsH: 0.0, adjustsW: 0.0) 79 | private var RKUserResizableViewUpperLeftAnchorPoint = RKUserResizableViewAnchorPoint(adjustsX: 1.0, adjustsY: 1.0, adjustsH: -1.0, adjustsW: 1.0) 80 | private var RKUserResizableViewMiddleLeftAnchorPoint = RKUserResizableViewAnchorPoint(adjustsX: 1.0, adjustsY: 0.0, adjustsH: 0.0, adjustsW: 1.0) 81 | private var RKUserResizableViewLowerLeftAnchorPoint = RKUserResizableViewAnchorPoint(adjustsX: 1.0, adjustsY: 0.0, adjustsH: 1.0, adjustsW: 1.0) 82 | private var RKUserResizableViewUpperMiddleAnchorPoint = RKUserResizableViewAnchorPoint(adjustsX: 0.0, adjustsY: 1.0, adjustsH: -1.0, adjustsW: 0.0) 83 | private var RKUserResizableViewUpperRightAnchorPoint = RKUserResizableViewAnchorPoint(adjustsX: 0.0, adjustsY: 1.0, adjustsH: -1.0, adjustsW: -1.0) 84 | private var RKUserResizableViewMiddleRightAnchorPoint = RKUserResizableViewAnchorPoint(adjustsX: 0.0, adjustsY: 0.0, adjustsH: 0.0, adjustsW: -1.0) 85 | private var RKUserResizableViewLowerRightAnchorPoint = RKUserResizableViewAnchorPoint(adjustsX: 0.0, adjustsY: 0.0, adjustsH: 1.0, adjustsW: -1.0) 86 | private var RKUserResizableViewLowerMiddleAnchorPoint = RKUserResizableViewAnchorPoint(adjustsX: 0.0, adjustsY: 0.0, adjustsH: 1.0, adjustsW: 0.0) 87 | 88 | func setupDefaultAttributes() { 89 | borderView = GripViewBorderView(frame: bounds.insetBy(dx: CGFloat(RKUserResizableViewGlobalInset), dy: CGFloat(RKUserResizableViewGlobalInset))) 90 | borderView?.isHidden = true 91 | self.addSubview(borderView!) 92 | minWidth = RKUserResizableViewDefaultMinWidth 93 | minHeight = RKUserResizableViewDefaultMinHeight 94 | isPreventsPositionOutsideSuperview = true 95 | } 96 | 97 | override init(frame: CGRect) { 98 | super.init(frame: frame) 99 | setupDefaultAttributes() 100 | } 101 | 102 | required init?(coder aDecoder: NSCoder) { 103 | super.init(coder: aDecoder) 104 | setupDefaultAttributes() 105 | } 106 | 107 | func setViewFrame(_ newFrame: CGRect) { 108 | self.frame = newFrame 109 | contentView?.frame = bounds.insetBy(dx: RKUserResizableViewGlobalInset + RKUserResizableViewInteractiveBorderSize / 2, dy: RKUserResizableViewGlobalInset + RKUserResizableViewInteractiveBorderSize / 2) 110 | borderView?.frame = bounds.insetBy(dx: RKUserResizableViewGlobalInset, dy: RKUserResizableViewGlobalInset) 111 | borderView?.setNeedsDisplay() 112 | 113 | } 114 | 115 | private func distanceBetweenTwoPoints(point1: CGPoint, point2: CGPoint) -> CGFloat { 116 | let dx: CGFloat = point2.x - point1.x 117 | let dy: CGFloat = point2.y - point1.y 118 | return sqrt(dx * dx + dy * dy) 119 | } 120 | 121 | func anchorPoint(forTouchLocation touchPoint: CGPoint) -> RKUserResizableViewAnchorPoint { 122 | // (1) Calculate the positions of each of the anchor points. 123 | let upperLeft = RKUserResizableViewAnchorPointPair(point: CGPoint(x: 0.0, y: 0.0), anchorPoint: RKUserResizableViewUpperLeftAnchorPoint) 124 | 125 | let upperMiddle = RKUserResizableViewAnchorPointPair(point: CGPoint(x: bounds.size.width / 2, y: 0.0), anchorPoint: RKUserResizableViewUpperMiddleAnchorPoint) 126 | 127 | let upperRight = RKUserResizableViewAnchorPointPair(point: CGPoint(x: bounds.size.width, y: 0.0), anchorPoint: RKUserResizableViewUpperRightAnchorPoint) 128 | 129 | let middleRight = RKUserResizableViewAnchorPointPair(point: CGPoint(x: bounds.size.width, y: bounds.size.height / 2), anchorPoint: RKUserResizableViewMiddleRightAnchorPoint) 130 | 131 | let lowerRight = RKUserResizableViewAnchorPointPair(point: CGPoint(x: bounds.size.width, y: bounds.size.height), anchorPoint: RKUserResizableViewLowerRightAnchorPoint) 132 | 133 | let lowerMiddle = RKUserResizableViewAnchorPointPair(point: CGPoint(x: bounds.size.width / 2, y: bounds.size.height), anchorPoint: RKUserResizableViewLowerMiddleAnchorPoint) 134 | 135 | let lowerLeft = RKUserResizableViewAnchorPointPair(point: CGPoint(x: 0, y: bounds.size.height), anchorPoint: RKUserResizableViewLowerLeftAnchorPoint) 136 | 137 | let middleLeft = RKUserResizableViewAnchorPointPair(point: CGPoint(x: 0, y: bounds.size.height / 2), anchorPoint: RKUserResizableViewMiddleLeftAnchorPoint) 138 | 139 | let centerPoint = RKUserResizableViewAnchorPointPair(point: CGPoint(x: bounds.size.width / 2, y: bounds.size.height / 2), anchorPoint: RKUserResizableViewNoResizeAnchorPoint) 140 | 141 | // (2) Iterate over each of the anchor points and find the one closest to the user's touch. 142 | let allPoints: [RKUserResizableViewAnchorPointPair] = [upperLeft, upperRight, lowerRight, lowerLeft, upperMiddle, lowerMiddle, middleLeft, middleRight, centerPoint] 143 | var smallestDistance: CGFloat = CGFloat(MAXFLOAT) 144 | var closestPoint: RKUserResizableViewAnchorPointPair = centerPoint 145 | for i in 0..<9 { 146 | let distance: CGFloat = distanceBetweenTwoPoints(point1: touchPoint, point2: allPoints[i].point) 147 | if distance < smallestDistance { 148 | closestPoint = allPoints[i] 149 | smallestDistance = distance 150 | } 151 | } 152 | return closestPoint.anchorPoint 153 | } 154 | 155 | func isResizing() -> Bool { 156 | return (anchorPoint.adjustsH != 0 || anchorPoint.adjustsW != 0 || anchorPoint.adjustsX != 0 || anchorPoint.adjustsY != 0) 157 | } 158 | 159 | override func touchesBegan(_ touches: Set, with event: UIEvent?) { 160 | super.touchesBegan(touches, with: event) 161 | // Notify the delegate we've begun our editing session. 162 | // if delegate?.responds(to: #selector(self.userResizableViewDidBeginEditing)) { 163 | delegate?.userResizableViewDidBeginEditing(self) 164 | // } 165 | borderView?.isHidden = false 166 | 167 | if let touch = touches.first { 168 | anchorPoint = anchorPoint(forTouchLocation: touch.location(in: self)) 169 | print(anchorPoint) 170 | // When resizing, all calculations are done in the superview's coordinate space. 171 | touchStart = touch.location(in: superview) 172 | if !isResizing() { 173 | // When translating, all calculations are done in the view's coordinate space. 174 | touchStart = touch.location(in: self) 175 | } 176 | } 177 | } 178 | 179 | override func touchesEnded(_ touches: Set, with event: UIEvent?) { 180 | // Notify the delegate we've ended our editing session. 181 | delegate?.userResizableViewDidEndEditing(self) 182 | } 183 | 184 | override func touchesCancelled(_ touches: Set, with event: UIEvent?) { 185 | // Notify the delegate we've ended our editing session. 186 | delegate?.userResizableViewDidEndEditing(self) 187 | } 188 | 189 | func showEditingHandles() { 190 | borderView?.isHidden = false 191 | } 192 | 193 | func hideEditingHandles() { 194 | borderView?.isHidden = true 195 | } 196 | 197 | func resize(usingTouchLocation touchPoint: CGPoint) { 198 | // (1) Update the touch point if we're outside the superview. 199 | /* disabled by rajat jain on 2017-08-28 200 | if isPreventsPositionOutsideSuperview { 201 | let border: CGFloat = RKUserResizableViewGlobalInset + RKUserResizableViewInteractiveBorderSize / 2 202 | if touchPoint.x < border { 203 | touchPoint.x = border 204 | } 205 | if touchPoint.x > superview?.bounds.size.width - border { 206 | touchPoint.x = superview?.bounds.size.width - border 207 | } 208 | if touchPoint.y < border { 209 | touchPoint.y = border 210 | } 211 | if touchPoint.y > superview?.bounds.size.height - border { 212 | touchPoint.y = superview?.bounds.size.height - border 213 | } 214 | } 215 | */ 216 | // (2) Calculate the deltas using the current anchor point. 217 | var deltaW: CGFloat = anchorPoint.adjustsW * (touchStart.x - touchPoint.x) 218 | let deltaX: CGFloat = anchorPoint.adjustsX * (-1.0 * deltaW) 219 | var deltaH: CGFloat = anchorPoint.adjustsH * (touchPoint.y - touchStart.y) 220 | let deltaY: CGFloat = anchorPoint.adjustsY * (-1.0 * deltaH) 221 | // (3) Calculate the new frame. 222 | var newX: CGFloat = frame.origin.x + deltaX 223 | var newY: CGFloat = frame.origin.y + deltaY 224 | var newWidth: CGFloat = frame.size.width + deltaW 225 | var newHeight: CGFloat = frame.size.height + deltaH 226 | // (4) If the new frame is too small, cancel the changes. 227 | if newWidth < minWidth { 228 | newWidth = frame.size.width 229 | newX = frame.origin.x 230 | } 231 | if newHeight < minHeight { 232 | newHeight = frame.size.height 233 | newY = frame.origin.y 234 | } 235 | 236 | // (5) Ensure the resize won't cause the view to move offscreen. 237 | if isPreventsPositionOutsideSuperview { 238 | if let superView = self.superview { 239 | if newX < superView.bounds.origin.x { 240 | // Calculate how much to grow the width by such that the new X coordintae will align with the superview. 241 | deltaW = self.frame.origin.x - superView.bounds.origin.x 242 | newWidth = self.frame.size.width + deltaW 243 | newX = superView.bounds.origin.x 244 | } 245 | 246 | if newX + newWidth > superView.bounds.origin.x + superView.bounds.size.width { 247 | newWidth = superView.bounds.size.width - newX 248 | } 249 | 250 | if newY < superView.bounds.origin.y { 251 | // Calculate how much to grow the height by such that the new Y coordintae will align with the superview. 252 | deltaH = self.frame.origin.y - superView.bounds.origin.y 253 | newHeight = self.frame.size.height + deltaH 254 | newY = superView.bounds.origin.y 255 | } 256 | 257 | if newY + newHeight > superView.bounds.origin.y + superView.bounds.size.height { 258 | newHeight = superView.bounds.size.height - newY 259 | } 260 | 261 | } 262 | } 263 | 264 | self.setViewFrame(CGRect(x: newX, y: newY, width: newWidth, height: newHeight)) 265 | 266 | touchStart = touchPoint 267 | } 268 | 269 | func translate(usingTouchLocation touchPoint: CGPoint) { 270 | var newCenter = CGPoint(x: center.x + touchPoint.x - touchStart.x, y: center.y + touchPoint.y - touchStart.y) 271 | if isPreventsPositionOutsideSuperview { 272 | if let superView = self.superview { 273 | 274 | // Ensure the translation won't cause the view to move offscreen. 275 | let midPointX: CGFloat = bounds.midX 276 | if newCenter.x > superView.bounds.size.width - midPointX { 277 | newCenter.x = superView.bounds.size.width - midPointX 278 | } 279 | if newCenter.x < midPointX { 280 | newCenter.x = midPointX 281 | } 282 | let midPointY: CGFloat = bounds.midY 283 | if newCenter.y > superView.bounds.size.height - midPointY { 284 | newCenter.y = superView.bounds.size.height - midPointY 285 | } 286 | if newCenter.y < midPointY { 287 | newCenter.y = midPointY 288 | } 289 | } 290 | } 291 | center = newCenter 292 | } 293 | 294 | override func touchesMoved(_ touches: Set, with event: UIEvent?) { 295 | if isResizing() { 296 | if let superView = self.superview { 297 | resize(usingTouchLocation: (touches.first?.location(in: superView))!) 298 | } 299 | } 300 | else { 301 | translate(usingTouchLocation: (touches.first?.location(in: self))!) 302 | } 303 | } 304 | 305 | deinit { 306 | contentView?.removeFromSuperview() 307 | } 308 | } 309 | 310 | 311 | class GripViewBorderView: UIView { 312 | 313 | let RKUserResizableViewGlobalInset:CGFloat = 5.0 314 | let RKUserResizableViewDefaultMinWidth:CGFloat = 48.0 315 | let RKUserResizableViewDefaultMinHeight:CGFloat = 48.0 316 | let RKUserResizableViewInteractiveBorderSize:CGFloat = 10.0 317 | 318 | weak var delegate: RKUserResizableViewDelegate? 319 | // Will be retained as a subview. 320 | var contentView: UIView? 321 | // Default is 48.0 for each. 322 | var minWidth: CGFloat = 0.0 323 | var minHeight: CGFloat = 0.0 324 | // Defaults to YES. Disables the user from dragging the view outside the parent view's bounds. 325 | var isPreventsPositionOutsideSuperview: Bool = false 326 | 327 | var borderView: GripViewBorderView? 328 | var touchStart = CGPoint.zero 329 | // Used to determine which components of the bounds we'll be modifying, based upon where the user's touch started. 330 | var anchorPoint = RKUserResizableViewAnchorPoint() 331 | 332 | func hideEditingHandles() { 333 | } 334 | 335 | func showEditingHandles() { 336 | } 337 | 338 | override init(frame: CGRect) { 339 | // Clear background to ensure the content view shows through. 340 | super.init(frame: frame) 341 | self.backgroundColor = .clear 342 | } 343 | 344 | required init?(coder aDecoder: NSCoder) { 345 | fatalError("init(coder:) has not been implemented") 346 | } 347 | 348 | override func draw(_ rect: CGRect) { 349 | let context: CGContext? = UIGraphicsGetCurrentContext() 350 | context?.saveGState() 351 | 352 | // (1) Draw the bounding box. 353 | context?.setLineWidth(1.0) 354 | context?.setStrokeColor(UIColor.blue.cgColor) 355 | context?.addRect(bounds.insetBy(dx: RKUserResizableViewInteractiveBorderSize / 2, dy: RKUserResizableViewInteractiveBorderSize / 2)) 356 | context?.strokePath() 357 | 358 | // (2) Calculate the bounding boxes for each of the anchor points. 359 | let upperLeft = CGRect(x: 0.0, y: 0.0, width: RKUserResizableViewInteractiveBorderSize, height: RKUserResizableViewInteractiveBorderSize) 360 | let upperRight = CGRect(x: bounds.size.width - RKUserResizableViewInteractiveBorderSize, y: 0.0, width: RKUserResizableViewInteractiveBorderSize, height: RKUserResizableViewInteractiveBorderSize) 361 | let lowerRight = CGRect(x: bounds.size.width - RKUserResizableViewInteractiveBorderSize, y: bounds.size.height - RKUserResizableViewInteractiveBorderSize, width: RKUserResizableViewInteractiveBorderSize, height: RKUserResizableViewInteractiveBorderSize) 362 | let lowerLeft = CGRect(x: 0.0, y: bounds.size.height - RKUserResizableViewInteractiveBorderSize, width: RKUserResizableViewInteractiveBorderSize, height: RKUserResizableViewInteractiveBorderSize) 363 | let upperMiddle = CGRect(x: (bounds.size.width - RKUserResizableViewInteractiveBorderSize) / 2, y: 0.0, width: RKUserResizableViewInteractiveBorderSize, height: RKUserResizableViewInteractiveBorderSize) 364 | let lowerMiddle = CGRect(x: (bounds.size.width - RKUserResizableViewInteractiveBorderSize) / 2, y: bounds.size.height - RKUserResizableViewInteractiveBorderSize, width: RKUserResizableViewInteractiveBorderSize, height: RKUserResizableViewInteractiveBorderSize) 365 | let middleLeft = CGRect(x: 0.0, y: (bounds.size.height - RKUserResizableViewInteractiveBorderSize) / 2, width: RKUserResizableViewInteractiveBorderSize, height: RKUserResizableViewInteractiveBorderSize) 366 | let middleRight = CGRect(x: bounds.size.width - RKUserResizableViewInteractiveBorderSize, y: (bounds.size.height - RKUserResizableViewInteractiveBorderSize) / 2, width: RKUserResizableViewInteractiveBorderSize, height: RKUserResizableViewInteractiveBorderSize) 367 | 368 | // (3) Create the gradient to paint the anchor points. 369 | let colors: [CGFloat] = [0.4, 0.8, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0] 370 | let baseSpace: CGColorSpace? = CGColorSpaceCreateDeviceRGB() 371 | let gradient: CGGradient = CGGradient(colorSpace: baseSpace!, colorComponents: colors, locations: nil, count: 2)! 372 | 373 | // (4) Set up the stroke for drawing the border of each of the anchor points. 374 | context?.setLineWidth(2) 375 | context!.setShadow(offset: CGSize(width: 0.5, height: 0.5), blur: 1) 376 | context?.setStrokeColor(UIColor.white.cgColor) 377 | 378 | // (5) Fill each anchor point using the gradient, then stroke the border. 379 | let allPoints: [CGRect] = [upperLeft, upperRight, lowerRight, lowerLeft, upperMiddle, lowerMiddle, middleLeft, middleRight] 380 | for i in 0..<8 { 381 | let currPoint: CGRect = allPoints[i] 382 | context?.saveGState() 383 | //context?.addEllipse(in: currPoint) 384 | let currentPoint = CGRect(x: currPoint.origin.x, y: currPoint.origin.y, width: 10, height: 10); 385 | context?.addEllipse(in: currentPoint) 386 | context?.clip() 387 | let startPoint = CGPoint(x: currentPoint.midX, y: currentPoint.minY) 388 | let endPoint = CGPoint(x: currentPoint.midX, y: currentPoint.maxY) 389 | context?.drawLinearGradient(gradient, start: startPoint, end: endPoint, options: []) 390 | context?.restoreGState() 391 | context?.strokeEllipse(in: currentPoint.insetBy(dx: 1, dy: 1)) 392 | } 393 | context?.restoreGState() 394 | } 395 | } 396 | 397 | --------------------------------------------------------------------------------