├── MaskableImageView.png ├── MaskableImageView ├── Assets.xcassets │ ├── Contents.json │ ├── Scampers 6685.imageset │ │ ├── Scampers 6685.jpg │ │ └── Contents.json │ ├── Gray Checkerboard.imageset │ │ ├── Gray Checkerboard.png │ │ └── Contents.json │ ├── AccentColor.colorset │ │ └── Contents.json │ └── AppIcon.appiconset │ │ └── Contents.json ├── TouchDownPanGestureRecognizer.swift ├── UIView+layerProperties.swift ├── AppDelegate.swift ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Info.plist ├── SceneDelegate.swift ├── ViewController.swift └── MaskableView.swift ├── MaskableImageView.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── xcuserdata │ └── duncan.xcuserdatad │ │ └── xcschemes │ │ └── xcschememanagement.plist └── project.pbxproj ├── MaskableImageViewTests ├── Info.plist └── MaskableImageViewTests.swift ├── MaskableImageViewUITests ├── Info.plist └── MaskableImageViewUITests.swift └── README.md /MaskableImageView.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DuncanMC/MaskableImageView/HEAD/MaskableImageView.png -------------------------------------------------------------------------------- /MaskableImageView/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /MaskableImageView/Assets.xcassets/Scampers 6685.imageset/Scampers 6685.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DuncanMC/MaskableImageView/HEAD/MaskableImageView/Assets.xcassets/Scampers 6685.imageset/Scampers 6685.jpg -------------------------------------------------------------------------------- /MaskableImageView/Assets.xcassets/Gray Checkerboard.imageset/Gray Checkerboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DuncanMC/MaskableImageView/HEAD/MaskableImageView/Assets.xcassets/Gray Checkerboard.imageset/Gray Checkerboard.png -------------------------------------------------------------------------------- /MaskableImageView.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /MaskableImageView/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /MaskableImageView.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /MaskableImageView/Assets.xcassets/Scampers 6685.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Scampers 6685.jpg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /MaskableImageView/Assets.xcassets/Gray Checkerboard.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Gray Checkerboard.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /MaskableImageView.xcodeproj/xcuserdata/duncan.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | MaskableImageView.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /MaskableImageView/TouchDownPanGestureRecognizer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TouchDownPanGestureRecognizer.swift 3 | // MaskableImageView 4 | // 5 | // Created by Duncan Champney on 5/31/21. 6 | // 7 | 8 | import UIKit 9 | 10 | /// This is a simple subclass of UIPanGestureRecognizer that begins sending notifications when the user first touches down 11 | /// rather than waiting until they begin dragging. 12 | class TouchDownPanGestureRecognizer: UIPanGestureRecognizer { 13 | override func touchesBegan(_ touches: Set, with event: UIEvent) { 14 | super.touchesBegan(touches, with: event) 15 | state = .began 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /MaskableImageViewTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /MaskableImageViewUITests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /MaskableImageView/UIView+layerProperties.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+borderColor.swift 3 | // AnimatePaths 4 | // 5 | // Created by Duncan Champney on 5/13/21. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | /** 12 | This extension exposes several properties of a UIView's backing CALayer as IBInspectable so they can be set in Storyboards. 13 | The `borderColor` var exposes the view's layer's borderColor as a UIColor rather than a CGColor, since Interface builder doesn't allow you to specify CGColors 14 | */ 15 | extension UIView { 16 | @IBInspectable @objc var borderColor: UIColor? { 17 | get { return layer.borderColor.map { UIColor(cgColor: $0) } } 18 | set { layer.borderColor = newValue?.cgColor } 19 | } 20 | @IBInspectable @objc var borderWidth: CGFloat { 21 | get { return layer.borderWidth } 22 | set { layer.borderWidth = newValue } 23 | } 24 | @IBInspectable @objc var cornerRadius: CGFloat { 25 | get { return layer.cornerRadius } 26 | set { layer.cornerRadius = newValue } 27 | } 28 | //cornerRadius 29 | } 30 | -------------------------------------------------------------------------------- /MaskableImageViewTests/MaskableImageViewTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MaskableImageViewTests.swift 3 | // MaskableImageViewTests 4 | // 5 | // Created by Duncan Champney on 5/30/21. 6 | // 7 | 8 | import XCTest 9 | @testable import MaskableImageView 10 | 11 | class MaskableImageViewTests: XCTestCase { 12 | 13 | override func setUpWithError() throws { 14 | // Put setup code here. This method is called before the invocation of each test method in the class. 15 | } 16 | 17 | override func tearDownWithError() throws { 18 | // Put teardown code here. This method is called after the invocation of each test method in the class. 19 | } 20 | 21 | func testExample() throws { 22 | // This is an example of a functional test case. 23 | // Use XCTAssert and related functions to verify your tests produce the correct results. 24 | } 25 | 26 | func testPerformanceExample() throws { 27 | // This is an example of a performance test case. 28 | self.measure { 29 | // Put the code you want to measure the time of here. 30 | } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /MaskableImageView/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // MaskableImageView 4 | // 5 | // Created by Duncan Champney on 5/30/21. 6 | // 7 | 8 | import UIKit 9 | 10 | @main 11 | class AppDelegate: UIResponder, UIApplicationDelegate { 12 | 13 | 14 | 15 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 16 | // Override point for customization after application launch. 17 | return true 18 | } 19 | 20 | // MARK: UISceneSession Lifecycle 21 | 22 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 23 | // Called when a new scene session is being created. 24 | // Use this method to select a configuration to create the new scene with. 25 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 26 | } 27 | 28 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 29 | // Called when the user discards a scene session. 30 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 31 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 32 | } 33 | 34 | 35 | } 36 | 37 | -------------------------------------------------------------------------------- /MaskableImageViewUITests/MaskableImageViewUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MaskableImageViewUITests.swift 3 | // MaskableImageViewUITests 4 | // 5 | // Created by Duncan Champney on 5/30/21. 6 | // 7 | 8 | import XCTest 9 | 10 | class MaskableImageViewUITests: XCTestCase { 11 | 12 | override func setUpWithError() throws { 13 | // Put setup code here. This method is called before the invocation of each test method in the class. 14 | 15 | // In UI tests it is usually best to stop immediately when a failure occurs. 16 | continueAfterFailure = false 17 | 18 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 19 | } 20 | 21 | override func tearDownWithError() throws { 22 | // Put teardown code here. This method is called after the invocation of each test method in the class. 23 | } 24 | 25 | func testExample() throws { 26 | // UI tests must launch the application that they test. 27 | let app = XCUIApplication() 28 | app.launch() 29 | 30 | // Use recording to get started writing UI tests. 31 | // Use XCTAssert and related functions to verify your tests produce the correct results. 32 | } 33 | 34 | func testLaunchPerformance() throws { 35 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, *) { 36 | // This measures how long it takes to launch your application. 37 | measure(metrics: [XCTApplicationLaunchMetric()]) { 38 | XCUIApplication().launch() 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /MaskableImageView/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 | -------------------------------------------------------------------------------- /MaskableImageView/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /MaskableImageView/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | UISceneConfigurations 28 | 29 | UIWindowSceneSessionRoleApplication 30 | 31 | 32 | UISceneConfigurationName 33 | Default Configuration 34 | UISceneDelegateClassName 35 | $(PRODUCT_MODULE_NAME).SceneDelegate 36 | UISceneStoryboardFile 37 | Main 38 | 39 | 40 | 41 | 42 | UIApplicationSupportsIndirectInputEvents 43 | 44 | UILaunchStoryboardName 45 | LaunchScreen 46 | UIMainStoryboardFile 47 | Main 48 | UIRequiredDeviceCapabilities 49 | 50 | armv7 51 | 52 | UISupportedInterfaceOrientations 53 | 54 | UIInterfaceOrientationPortrait 55 | 56 | UISupportedInterfaceOrientations~ipad 57 | 58 | UIInterfaceOrientationPortrait 59 | UIInterfaceOrientationPortraitUpsideDown 60 | UIInterfaceOrientationLandscapeLeft 61 | UIInterfaceOrientationLandscapeRight 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /MaskableImageView/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // MaskableImageView 4 | // 5 | // Created by Duncan Champney on 5/30/21. 6 | // 7 | 8 | import UIKit 9 | 10 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 11 | 12 | var window: UIWindow? 13 | 14 | 15 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 16 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 17 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 18 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 19 | guard let _ = (scene as? UIWindowScene) else { return } 20 | } 21 | 22 | func sceneDidDisconnect(_ scene: UIScene) { 23 | // Called as the scene is being released by the system. 24 | // This occurs shortly after the scene enters the background, or when its session is discarded. 25 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 26 | // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). 27 | } 28 | 29 | func sceneDidBecomeActive(_ scene: UIScene) { 30 | // Called when the scene has moved from an inactive state to an active state. 31 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 32 | } 33 | 34 | func sceneWillResignActive(_ scene: UIScene) { 35 | // Called when the scene will move from an active state to an inactive state. 36 | // This may occur due to temporary interruptions (ex. an incoming phone call). 37 | } 38 | 39 | func sceneWillEnterForeground(_ scene: UIScene) { 40 | // Called as the scene transitions from the background to the foreground. 41 | // Use this method to undo the changes made on entering the background. 42 | } 43 | 44 | func sceneDidEnterBackground(_ scene: UIScene) { 45 | // Called as the scene transitions from the foreground to the background. 46 | // Use this method to save data, release shared resources, and store enough scene-specific state information 47 | // to restore the scene back to its current state. 48 | } 49 | 50 | 51 | } 52 | 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## MaskableImageView 2 | 3 | 4 | This project demonstrates how to use a `CALayer` to mask a `UIView`. 5 | 6 | It defines a custom subclass of UIImageView, `MaskableView`. 7 | 8 | The `MaskableView` class has a property `maskLayer` that contains a CALayer. 9 | 10 | `MaskableView` implements an updateBounds() method property so that when the view's bounds change, it's view controller can notify it so it can update it's mask layer and rebuild it's sample mask image. 11 | 12 | The `MaskableView ` has a method `installSampleMask` which builds an image the same size as the image view, mostly filled with opaque black, but with a small rectangle in the center filled with black at an alpha of 0.5. The translucent center rectangle causes the image view to become partly transparent and show the view underneath. 13 | 14 | The demo app installs a couple of subviews into the `MaskableView`, a sample image of Scampers, one of my dogs, and a UILabel. It also installs an image of a checkerboard under the `MaskableView ` so that you can see the translucent parts more easily. 15 | 16 | *** 17 | 18 | ### The MaskableView class 19 | 20 | The `MaskableView` has properties `circleRadius`, `maskDrawingAlpha`, and `drawingAction` that it uses to let the user erase/un-erase the image by tapping on the view to update the mask. 21 | 22 | It attaches a custom subclass of `UIPanGestureRecognizer`, a `TouchDownPanGestureRecognizer`, to itself, with an action of `gestureRecognizerUpdate`. The `gestureRecognizerUpdate` method takes the tap/drag location from the gesture recognizer and uses it to draw a circle onto the image mask that either decreases the image mask's alpha (to partly erase pixels) or increase the image mask's alpha (to make those pixels more opaque.) The custom `TouchDownPanGestureRecognizer` is a very minor change to a normal `UIPanGestureRecognizer` that begins sending events on the first touch event rather than waiting for the user to drag. 23 | 24 | It also has a property `circleCursorColor:UIColor`. It defaults to clear. If you set `circleCursorColor` to a non-clear color the `MaskableView` draws a "circle cursor" at the current tap point to show the shape the user is drawing into. The demo app's view controller sets the `circleCursorColor` to a slightly translucent yellow. 25 | 26 | Its mask drawing is crude, and only meant for demonstration purposes. It draws a series of discrete circles intstead of rendering a path into the mask based on the user's drag gesture. A better solution would be to connect the points from the gesture recognizer and use them to render a smoothed curve into the mask. 27 | 28 | *** 29 | 30 | The app's screen looks like this: 31 | 32 | 33 | ![](MaskableImageView.png) -------------------------------------------------------------------------------- /MaskableImageView/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // MaskableImageView 4 | // 5 | // Created by Duncan Champney on 5/30/21. 6 | // 7 | 8 | import UIKit 9 | 10 | class ViewController: UIViewController { 11 | 12 | @IBOutlet weak var circleRadiusLabel: UILabel! 13 | @IBOutlet weak var circleRadiusSlider: UISlider! 14 | 15 | @IBOutlet weak var alphaLabel: UILabel! 16 | @IBOutlet weak var alphaSlider: UISlider! 17 | 18 | var maskDrawingAlpha: CGFloat = 0 { 19 | didSet { 20 | maskableView.maskDrawingAlpha = maskDrawingAlpha 21 | alphaLabel.text = String(format: "%.2f", maskDrawingAlpha) 22 | alphaSlider.value = Float(maskDrawingAlpha) 23 | } 24 | } 25 | var circleRadius: CGFloat = 0 26 | { 27 | didSet { 28 | maskableView.circleRadius = circleRadius 29 | circleRadiusLabel.text = String(format: "%.1f", circleRadius) 30 | circleRadiusSlider.value = Float(circleRadius) 31 | } 32 | } 33 | 34 | @IBOutlet weak var maskableView: MaskableView! 35 | 36 | var maskableViewBounds = CGRect.zero 37 | 38 | override func viewDidLoad() { 39 | super.viewDidLoad() 40 | // Do any additional setup after loading the view. 41 | } 42 | 43 | @IBAction func handleEraseRevealControl(_ sender: UISegmentedControl) { 44 | if let drawingAction = DrawingAction(rawValue: sender.selectedSegmentIndex) { 45 | maskableView.drawingAction = drawingAction 46 | } 47 | } 48 | @IBAction func handleCircleRadiusSlider(_ sender: UISlider) { 49 | circleRadius = CGFloat(sender.value) 50 | } 51 | 52 | @IBAction func handleAlphaSlider(_ sender: UISlider) { 53 | maskDrawingAlpha = CGFloat(sender.value) 54 | } 55 | 56 | @IBAction func handleSaveButton(_ sender: UIButton) { 57 | print("In handleSaveButton") 58 | if let image = maskableView.image, 59 | let pngData = image.pngData(){ 60 | print(image.description) 61 | let imageURL = getDocumentsDirectory().appendingPathComponent("image.png", isDirectory: false) 62 | do { 63 | try pngData.write(to: imageURL) 64 | print("Wrote png to \(imageURL.path)") 65 | } 66 | catch { 67 | print("Error writing file to \(imageURL.path)") 68 | } 69 | } 70 | } 71 | 72 | func getDocumentsDirectory() -> URL { 73 | let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) 74 | let documentsDirectory = paths[0] 75 | return documentsDirectory 76 | } 77 | override func viewDidAppear(_ animated: Bool) { 78 | super.viewDidAppear(animated) 79 | circleRadius = 20 80 | maskDrawingAlpha = 0.5 81 | maskableView.circleCursorColor = UIColor(red: 1, green: 1, blue: 0, alpha: 0.8) 82 | maskableView.outerCircleCursorColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.5) 83 | } 84 | 85 | override func viewDidLayoutSubviews() { 86 | if maskableView.bounds != maskableViewBounds { 87 | maskableViewBounds = maskableView.bounds 88 | maskableView.updateBounds() 89 | } 90 | } 91 | } 92 | 93 | -------------------------------------------------------------------------------- /MaskableImageView/MaskableView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // 3 | // MaskableView 4 | // 5 | // Created by Duncan Champney on 5/30/21. 6 | // 7 | 8 | import UIKit 9 | 10 | enum DrawingAction: Int { 11 | case erase = 0 12 | case draw = 1 13 | } 14 | 15 | import UIKit.UIGestureRecognizerSubclass 16 | 17 | 18 | class MaskableView: UIView { 19 | 20 | public var drawingAction: DrawingAction = .erase 21 | @objc @IBInspectable public var circleRadius: CGFloat = 20 22 | public var maskDrawingAlpha: CGFloat = 1.0 23 | public var image: UIImage? { 24 | guard let renderer = renderer else { return nil} 25 | let result = renderer.image { 26 | context in 27 | 28 | return layer.render(in: context.cgContext) 29 | } 30 | return result 31 | } 32 | 33 | /// This color is used to draw the "cursor" around the circle shape being drawn onto the mask layer. By default the color is nil (no cursor) 34 | /// Set a color if you want to stroke the circle being drawn. 35 | @objc @IBInspectable public var circleCursorColor: UIColor? = nil { 36 | didSet { shapeLayer.strokeColor = circleCursorColor?.cgColor } 37 | } 38 | 39 | /// This color is used to draw an outer circle around the circle shape being drawn onto the mask layer. By default the color is nil (no cursor) 40 | /// Use a outerCircleCursorColor that contrasts with the circleCursorColor 41 | /// (e.g. use a dark outerCircleCursorColor for a light circleCursorColor) 42 | @objc @IBInspectable public var outerCircleCursorColor: UIColor? = nil { 43 | didSet { outerShapeLayer.strokeColor = outerCircleCursorColor?.cgColor } 44 | } 45 | 46 | // MARK: - Private vars 47 | 48 | private var maskImage: UIImage? = nil 49 | private var maskLayer = CALayer() 50 | private var shapeLayer = CAShapeLayer() 51 | private var outerShapeLayer = CAShapeLayer() 52 | private var renderer: UIGraphicsImageRenderer? 53 | private var panGestureRecognizer = TouchDownPanGestureRecognizer() 54 | 55 | private var firstTime = true 56 | 57 | // MARK: - Public functions 58 | 59 | public func updateBounds() { 60 | maskLayer.frame = layer.bounds 61 | shapeLayer.frame = layer.frame 62 | outerShapeLayer.frame = layer.frame 63 | if firstTime { 64 | renderer = UIGraphicsImageRenderer(size: bounds.size) 65 | installSampleMask() 66 | layer.superlayer?.addSublayer(shapeLayer) 67 | layer.superlayer?.addSublayer(outerShapeLayer) 68 | firstTime = false 69 | } else { 70 | guard let renderer = renderer else { return } 71 | let image = renderer.image { (context) in 72 | if let maskImage = maskImage { 73 | maskImage.draw(in: bounds) 74 | } 75 | } 76 | maskImage = image 77 | maskLayer.contents = maskImage?.cgImage 78 | 79 | } 80 | } 81 | 82 | private func installSampleMask() { 83 | guard let renderer = renderer else { return } 84 | let image = renderer.image { (ctx) in 85 | UIColor.black.setFill() 86 | ctx.fill(bounds, blendMode: .normal) 87 | let insetRect = bounds.insetBy(dx: bounds.width / 4, dy: bounds.height/4) 88 | UIColor(red: 0, green: 0, blue: 0, alpha: 0.5).setFill() 89 | ctx.fill(insetRect) 90 | } 91 | maskImage = image 92 | maskLayer.contents = maskImage?.cgImage 93 | 94 | } 95 | 96 | private func drawCircleAtPoint(point: CGPoint) { 97 | guard let renderer = renderer else { return } 98 | let image = renderer.image { (context) in 99 | if let maskImage = maskImage { 100 | maskImage.draw(in: bounds) 101 | let rect = CGRect(origin: point, size: CGSize.zero).insetBy(dx: -circleRadius/2, dy: -circleRadius/2) 102 | let color = UIColor.black.cgColor 103 | context.cgContext.setFillColor(color) 104 | let blendMode: CGBlendMode 105 | let alpha: CGFloat 106 | if drawingAction == .erase { 107 | // This is what I worked out to reduce the alpha of the mask by maskDrawingAlpha in the drawing area 108 | blendMode = .sourceIn 109 | alpha = 1 - maskDrawingAlpha 110 | } else { 111 | // In normal drawing mode the new drawing alpha is added to the alpha of the existing area. 112 | blendMode = .normal 113 | alpha = maskDrawingAlpha 114 | } 115 | 116 | if circleCursorColor != nil { 117 | let circlePath = UIBezierPath(ovalIn:rect) 118 | circlePath.fill(with: blendMode, alpha: alpha) 119 | shapeLayer.path = circlePath.cgPath 120 | } 121 | 122 | if outerCircleCursorColor != nil { 123 | let outerRect = rect.insetBy(dx: -2, dy: -2) 124 | let outerCirclePath = UIBezierPath(ovalIn:outerRect) 125 | outerCirclePath.fill(with: blendMode, alpha: alpha) 126 | outerShapeLayer.path = outerCirclePath.cgPath 127 | } 128 | } 129 | } 130 | maskImage = image 131 | maskLayer.contents = maskImage?.cgImage 132 | } 133 | 134 | // MARK: - IBAction methods 135 | 136 | // Erase/un-erase the point from the tap/pan gesture recognzier 137 | @IBAction func gestureRecognizerUpdate(_ sender: UIGestureRecognizer) { 138 | let point = sender.location(in: self) 139 | if sender.state != .ended { 140 | drawCircleAtPoint(point: point) 141 | } else { 142 | self.shapeLayer.path = nil 143 | self.outerShapeLayer.path = nil 144 | } 145 | } 146 | 147 | // MARK: - Object lifecycle methods 148 | 149 | override init(frame: CGRect) { 150 | super.init(frame: frame) 151 | doInitSetup() 152 | } 153 | 154 | required init?(coder: NSCoder) { 155 | super.init(coder: coder) 156 | doInitSetup() 157 | } 158 | 159 | func doInitSetup() { 160 | shapeLayer.strokeColor = circleCursorColor?.cgColor 161 | shapeLayer.lineWidth = 2 162 | shapeLayer.fillColor = UIColor.clear.cgColor 163 | outerShapeLayer.strokeColor = outerCircleCursorColor?.cgColor 164 | outerShapeLayer.lineWidth = 1 165 | outerShapeLayer.fillColor = UIColor.clear.cgColor 166 | 167 | layer.mask = maskLayer 168 | 169 | // Set up a pan gesture recognizer to erase/un-erase a series of circles as the user drags over the image. 170 | panGestureRecognizer.addTarget(self, action: #selector(gestureRecognizerUpdate(_:))) 171 | self.addGestureRecognizer(panGestureRecognizer) 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /MaskableImageView/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 | 28 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 67 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 119 | 120 | 121 | 122 | 123 | 124 | 133 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | -------------------------------------------------------------------------------- /MaskableImageView.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | AE599A122663DD7500B6538D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE599A112663DD7500B6538D /* AppDelegate.swift */; }; 11 | AE599A142663DD7500B6538D /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE599A132663DD7500B6538D /* SceneDelegate.swift */; }; 12 | AE599A162663DD7500B6538D /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE599A152663DD7500B6538D /* ViewController.swift */; }; 13 | AE599A192663DD7500B6538D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AE599A172663DD7500B6538D /* Main.storyboard */; }; 14 | AE599A1B2663DD7800B6538D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AE599A1A2663DD7800B6538D /* Assets.xcassets */; }; 15 | AE599A1E2663DD7800B6538D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AE599A1C2663DD7800B6538D /* LaunchScreen.storyboard */; }; 16 | AE599A292663DD7900B6538D /* MaskableImageViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE599A282663DD7900B6538D /* MaskableImageViewTests.swift */; }; 17 | AE599A342663DD7900B6538D /* MaskableImageViewUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE599A332663DD7900B6538D /* MaskableImageViewUITests.swift */; }; 18 | AE599A452663DD9400B6538D /* MaskableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE599A442663DD9400B6538D /* MaskableView.swift */; }; 19 | AEB083D526651F4D00E360DC /* TouchDownPanGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEB083D426651F4D00E360DC /* TouchDownPanGestureRecognizer.swift */; }; 20 | AEB083DD26652B0A00E360DC /* UIView+layerProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEB083DC26652B0A00E360DC /* UIView+layerProperties.swift */; }; 21 | /* End PBXBuildFile section */ 22 | 23 | /* Begin PBXContainerItemProxy section */ 24 | AE599A252663DD7900B6538D /* PBXContainerItemProxy */ = { 25 | isa = PBXContainerItemProxy; 26 | containerPortal = AE599A062663DD7500B6538D /* Project object */; 27 | proxyType = 1; 28 | remoteGlobalIDString = AE599A0D2663DD7500B6538D; 29 | remoteInfo = MaskableImageView; 30 | }; 31 | AE599A302663DD7900B6538D /* PBXContainerItemProxy */ = { 32 | isa = PBXContainerItemProxy; 33 | containerPortal = AE599A062663DD7500B6538D /* Project object */; 34 | proxyType = 1; 35 | remoteGlobalIDString = AE599A0D2663DD7500B6538D; 36 | remoteInfo = MaskableImageView; 37 | }; 38 | /* End PBXContainerItemProxy section */ 39 | 40 | /* Begin PBXFileReference section */ 41 | AE599A0E2663DD7500B6538D /* MaskableImageView.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MaskableImageView.app; sourceTree = BUILT_PRODUCTS_DIR; }; 42 | AE599A112663DD7500B6538D /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 43 | AE599A132663DD7500B6538D /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 44 | AE599A152663DD7500B6538D /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 45 | AE599A182663DD7500B6538D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 46 | AE599A1A2663DD7800B6538D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 47 | AE599A1D2663DD7800B6538D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 48 | AE599A1F2663DD7800B6538D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 49 | AE599A242663DD7900B6538D /* MaskableImageViewTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MaskableImageViewTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 50 | AE599A282663DD7900B6538D /* MaskableImageViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaskableImageViewTests.swift; sourceTree = ""; }; 51 | AE599A2A2663DD7900B6538D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 52 | AE599A2F2663DD7900B6538D /* MaskableImageViewUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MaskableImageViewUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 53 | AE599A332663DD7900B6538D /* MaskableImageViewUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaskableImageViewUITests.swift; sourceTree = ""; }; 54 | AE599A352663DD7900B6538D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 55 | AE599A442663DD9400B6538D /* MaskableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaskableView.swift; sourceTree = ""; }; 56 | AEB083D426651F4D00E360DC /* TouchDownPanGestureRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TouchDownPanGestureRecognizer.swift; sourceTree = ""; }; 57 | AEB083DC26652B0A00E360DC /* UIView+layerProperties.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIView+layerProperties.swift"; sourceTree = ""; }; 58 | /* End PBXFileReference section */ 59 | 60 | /* Begin PBXFrameworksBuildPhase section */ 61 | AE599A0B2663DD7500B6538D /* Frameworks */ = { 62 | isa = PBXFrameworksBuildPhase; 63 | buildActionMask = 2147483647; 64 | files = ( 65 | ); 66 | runOnlyForDeploymentPostprocessing = 0; 67 | }; 68 | AE599A212663DD7900B6538D /* Frameworks */ = { 69 | isa = PBXFrameworksBuildPhase; 70 | buildActionMask = 2147483647; 71 | files = ( 72 | ); 73 | runOnlyForDeploymentPostprocessing = 0; 74 | }; 75 | AE599A2C2663DD7900B6538D /* Frameworks */ = { 76 | isa = PBXFrameworksBuildPhase; 77 | buildActionMask = 2147483647; 78 | files = ( 79 | ); 80 | runOnlyForDeploymentPostprocessing = 0; 81 | }; 82 | /* End PBXFrameworksBuildPhase section */ 83 | 84 | /* Begin PBXGroup section */ 85 | AE599A052663DD7500B6538D = { 86 | isa = PBXGroup; 87 | children = ( 88 | AE599A102663DD7500B6538D /* MaskableImageView */, 89 | AE599A272663DD7900B6538D /* MaskableImageViewTests */, 90 | AE599A322663DD7900B6538D /* MaskableImageViewUITests */, 91 | AE599A0F2663DD7500B6538D /* Products */, 92 | ); 93 | sourceTree = ""; 94 | }; 95 | AE599A0F2663DD7500B6538D /* Products */ = { 96 | isa = PBXGroup; 97 | children = ( 98 | AE599A0E2663DD7500B6538D /* MaskableImageView.app */, 99 | AE599A242663DD7900B6538D /* MaskableImageViewTests.xctest */, 100 | AE599A2F2663DD7900B6538D /* MaskableImageViewUITests.xctest */, 101 | ); 102 | name = Products; 103 | sourceTree = ""; 104 | }; 105 | AE599A102663DD7500B6538D /* MaskableImageView */ = { 106 | isa = PBXGroup; 107 | children = ( 108 | AE599A112663DD7500B6538D /* AppDelegate.swift */, 109 | AEB083D426651F4D00E360DC /* TouchDownPanGestureRecognizer.swift */, 110 | AE599A132663DD7500B6538D /* SceneDelegate.swift */, 111 | AEB083DC26652B0A00E360DC /* UIView+layerProperties.swift */, 112 | AE599A442663DD9400B6538D /* MaskableView.swift */, 113 | AE599A152663DD7500B6538D /* ViewController.swift */, 114 | AE599A172663DD7500B6538D /* Main.storyboard */, 115 | AE599A1A2663DD7800B6538D /* Assets.xcassets */, 116 | AE599A1C2663DD7800B6538D /* LaunchScreen.storyboard */, 117 | AE599A1F2663DD7800B6538D /* Info.plist */, 118 | ); 119 | path = MaskableImageView; 120 | sourceTree = ""; 121 | }; 122 | AE599A272663DD7900B6538D /* MaskableImageViewTests */ = { 123 | isa = PBXGroup; 124 | children = ( 125 | AE599A282663DD7900B6538D /* MaskableImageViewTests.swift */, 126 | AE599A2A2663DD7900B6538D /* Info.plist */, 127 | ); 128 | path = MaskableImageViewTests; 129 | sourceTree = ""; 130 | }; 131 | AE599A322663DD7900B6538D /* MaskableImageViewUITests */ = { 132 | isa = PBXGroup; 133 | children = ( 134 | AE599A332663DD7900B6538D /* MaskableImageViewUITests.swift */, 135 | AE599A352663DD7900B6538D /* Info.plist */, 136 | ); 137 | path = MaskableImageViewUITests; 138 | sourceTree = ""; 139 | }; 140 | /* End PBXGroup section */ 141 | 142 | /* Begin PBXNativeTarget section */ 143 | AE599A0D2663DD7500B6538D /* MaskableImageView */ = { 144 | isa = PBXNativeTarget; 145 | buildConfigurationList = AE599A382663DD7900B6538D /* Build configuration list for PBXNativeTarget "MaskableImageView" */; 146 | buildPhases = ( 147 | AE599A0A2663DD7500B6538D /* Sources */, 148 | AE599A0B2663DD7500B6538D /* Frameworks */, 149 | AE599A0C2663DD7500B6538D /* Resources */, 150 | ); 151 | buildRules = ( 152 | ); 153 | dependencies = ( 154 | ); 155 | name = MaskableImageView; 156 | productName = MaskableImageView; 157 | productReference = AE599A0E2663DD7500B6538D /* MaskableImageView.app */; 158 | productType = "com.apple.product-type.application"; 159 | }; 160 | AE599A232663DD7900B6538D /* MaskableImageViewTests */ = { 161 | isa = PBXNativeTarget; 162 | buildConfigurationList = AE599A3B2663DD7900B6538D /* Build configuration list for PBXNativeTarget "MaskableImageViewTests" */; 163 | buildPhases = ( 164 | AE599A202663DD7900B6538D /* Sources */, 165 | AE599A212663DD7900B6538D /* Frameworks */, 166 | AE599A222663DD7900B6538D /* Resources */, 167 | ); 168 | buildRules = ( 169 | ); 170 | dependencies = ( 171 | AE599A262663DD7900B6538D /* PBXTargetDependency */, 172 | ); 173 | name = MaskableImageViewTests; 174 | productName = MaskableImageViewTests; 175 | productReference = AE599A242663DD7900B6538D /* MaskableImageViewTests.xctest */; 176 | productType = "com.apple.product-type.bundle.unit-test"; 177 | }; 178 | AE599A2E2663DD7900B6538D /* MaskableImageViewUITests */ = { 179 | isa = PBXNativeTarget; 180 | buildConfigurationList = AE599A3E2663DD7900B6538D /* Build configuration list for PBXNativeTarget "MaskableImageViewUITests" */; 181 | buildPhases = ( 182 | AE599A2B2663DD7900B6538D /* Sources */, 183 | AE599A2C2663DD7900B6538D /* Frameworks */, 184 | AE599A2D2663DD7900B6538D /* Resources */, 185 | ); 186 | buildRules = ( 187 | ); 188 | dependencies = ( 189 | AE599A312663DD7900B6538D /* PBXTargetDependency */, 190 | ); 191 | name = MaskableImageViewUITests; 192 | productName = MaskableImageViewUITests; 193 | productReference = AE599A2F2663DD7900B6538D /* MaskableImageViewUITests.xctest */; 194 | productType = "com.apple.product-type.bundle.ui-testing"; 195 | }; 196 | /* End PBXNativeTarget section */ 197 | 198 | /* Begin PBXProject section */ 199 | AE599A062663DD7500B6538D /* Project object */ = { 200 | isa = PBXProject; 201 | attributes = { 202 | LastSwiftUpdateCheck = 1240; 203 | LastUpgradeCheck = 1240; 204 | TargetAttributes = { 205 | AE599A0D2663DD7500B6538D = { 206 | CreatedOnToolsVersion = 12.4; 207 | }; 208 | AE599A232663DD7900B6538D = { 209 | CreatedOnToolsVersion = 12.4; 210 | TestTargetID = AE599A0D2663DD7500B6538D; 211 | }; 212 | AE599A2E2663DD7900B6538D = { 213 | CreatedOnToolsVersion = 12.4; 214 | TestTargetID = AE599A0D2663DD7500B6538D; 215 | }; 216 | }; 217 | }; 218 | buildConfigurationList = AE599A092663DD7500B6538D /* Build configuration list for PBXProject "MaskableImageView" */; 219 | compatibilityVersion = "Xcode 9.3"; 220 | developmentRegion = en; 221 | hasScannedForEncodings = 0; 222 | knownRegions = ( 223 | en, 224 | Base, 225 | ); 226 | mainGroup = AE599A052663DD7500B6538D; 227 | productRefGroup = AE599A0F2663DD7500B6538D /* Products */; 228 | projectDirPath = ""; 229 | projectRoot = ""; 230 | targets = ( 231 | AE599A0D2663DD7500B6538D /* MaskableImageView */, 232 | AE599A232663DD7900B6538D /* MaskableImageViewTests */, 233 | AE599A2E2663DD7900B6538D /* MaskableImageViewUITests */, 234 | ); 235 | }; 236 | /* End PBXProject section */ 237 | 238 | /* Begin PBXResourcesBuildPhase section */ 239 | AE599A0C2663DD7500B6538D /* Resources */ = { 240 | isa = PBXResourcesBuildPhase; 241 | buildActionMask = 2147483647; 242 | files = ( 243 | AE599A1E2663DD7800B6538D /* LaunchScreen.storyboard in Resources */, 244 | AE599A1B2663DD7800B6538D /* Assets.xcassets in Resources */, 245 | AE599A192663DD7500B6538D /* Main.storyboard in Resources */, 246 | ); 247 | runOnlyForDeploymentPostprocessing = 0; 248 | }; 249 | AE599A222663DD7900B6538D /* Resources */ = { 250 | isa = PBXResourcesBuildPhase; 251 | buildActionMask = 2147483647; 252 | files = ( 253 | ); 254 | runOnlyForDeploymentPostprocessing = 0; 255 | }; 256 | AE599A2D2663DD7900B6538D /* Resources */ = { 257 | isa = PBXResourcesBuildPhase; 258 | buildActionMask = 2147483647; 259 | files = ( 260 | ); 261 | runOnlyForDeploymentPostprocessing = 0; 262 | }; 263 | /* End PBXResourcesBuildPhase section */ 264 | 265 | /* Begin PBXSourcesBuildPhase section */ 266 | AE599A0A2663DD7500B6538D /* Sources */ = { 267 | isa = PBXSourcesBuildPhase; 268 | buildActionMask = 2147483647; 269 | files = ( 270 | AE599A452663DD9400B6538D /* MaskableView.swift in Sources */, 271 | AE599A162663DD7500B6538D /* ViewController.swift in Sources */, 272 | AEB083D526651F4D00E360DC /* TouchDownPanGestureRecognizer.swift in Sources */, 273 | AE599A122663DD7500B6538D /* AppDelegate.swift in Sources */, 274 | AE599A142663DD7500B6538D /* SceneDelegate.swift in Sources */, 275 | AEB083DD26652B0A00E360DC /* UIView+layerProperties.swift in Sources */, 276 | ); 277 | runOnlyForDeploymentPostprocessing = 0; 278 | }; 279 | AE599A202663DD7900B6538D /* Sources */ = { 280 | isa = PBXSourcesBuildPhase; 281 | buildActionMask = 2147483647; 282 | files = ( 283 | AE599A292663DD7900B6538D /* MaskableImageViewTests.swift in Sources */, 284 | ); 285 | runOnlyForDeploymentPostprocessing = 0; 286 | }; 287 | AE599A2B2663DD7900B6538D /* Sources */ = { 288 | isa = PBXSourcesBuildPhase; 289 | buildActionMask = 2147483647; 290 | files = ( 291 | AE599A342663DD7900B6538D /* MaskableImageViewUITests.swift in Sources */, 292 | ); 293 | runOnlyForDeploymentPostprocessing = 0; 294 | }; 295 | /* End PBXSourcesBuildPhase section */ 296 | 297 | /* Begin PBXTargetDependency section */ 298 | AE599A262663DD7900B6538D /* PBXTargetDependency */ = { 299 | isa = PBXTargetDependency; 300 | target = AE599A0D2663DD7500B6538D /* MaskableImageView */; 301 | targetProxy = AE599A252663DD7900B6538D /* PBXContainerItemProxy */; 302 | }; 303 | AE599A312663DD7900B6538D /* PBXTargetDependency */ = { 304 | isa = PBXTargetDependency; 305 | target = AE599A0D2663DD7500B6538D /* MaskableImageView */; 306 | targetProxy = AE599A302663DD7900B6538D /* PBXContainerItemProxy */; 307 | }; 308 | /* End PBXTargetDependency section */ 309 | 310 | /* Begin PBXVariantGroup section */ 311 | AE599A172663DD7500B6538D /* Main.storyboard */ = { 312 | isa = PBXVariantGroup; 313 | children = ( 314 | AE599A182663DD7500B6538D /* Base */, 315 | ); 316 | name = Main.storyboard; 317 | sourceTree = ""; 318 | }; 319 | AE599A1C2663DD7800B6538D /* LaunchScreen.storyboard */ = { 320 | isa = PBXVariantGroup; 321 | children = ( 322 | AE599A1D2663DD7800B6538D /* Base */, 323 | ); 324 | name = LaunchScreen.storyboard; 325 | sourceTree = ""; 326 | }; 327 | /* End PBXVariantGroup section */ 328 | 329 | /* Begin XCBuildConfiguration section */ 330 | AE599A362663DD7900B6538D /* Debug */ = { 331 | isa = XCBuildConfiguration; 332 | buildSettings = { 333 | ALWAYS_SEARCH_USER_PATHS = NO; 334 | CLANG_ANALYZER_NONNULL = YES; 335 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 336 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 337 | CLANG_CXX_LIBRARY = "libc++"; 338 | CLANG_ENABLE_MODULES = YES; 339 | CLANG_ENABLE_OBJC_ARC = YES; 340 | CLANG_ENABLE_OBJC_WEAK = YES; 341 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 342 | CLANG_WARN_BOOL_CONVERSION = YES; 343 | CLANG_WARN_COMMA = YES; 344 | CLANG_WARN_CONSTANT_CONVERSION = YES; 345 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 346 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 347 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 348 | CLANG_WARN_EMPTY_BODY = YES; 349 | CLANG_WARN_ENUM_CONVERSION = YES; 350 | CLANG_WARN_INFINITE_RECURSION = YES; 351 | CLANG_WARN_INT_CONVERSION = YES; 352 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 353 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 354 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 355 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 356 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 357 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 358 | CLANG_WARN_STRICT_PROTOTYPES = YES; 359 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 360 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 361 | CLANG_WARN_UNREACHABLE_CODE = YES; 362 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 363 | COPY_PHASE_STRIP = NO; 364 | DEBUG_INFORMATION_FORMAT = dwarf; 365 | ENABLE_STRICT_OBJC_MSGSEND = YES; 366 | ENABLE_TESTABILITY = YES; 367 | GCC_C_LANGUAGE_STANDARD = gnu11; 368 | GCC_DYNAMIC_NO_PIC = NO; 369 | GCC_NO_COMMON_BLOCKS = YES; 370 | GCC_OPTIMIZATION_LEVEL = 0; 371 | GCC_PREPROCESSOR_DEFINITIONS = ( 372 | "DEBUG=1", 373 | "$(inherited)", 374 | ); 375 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 376 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 377 | GCC_WARN_UNDECLARED_SELECTOR = YES; 378 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 379 | GCC_WARN_UNUSED_FUNCTION = YES; 380 | GCC_WARN_UNUSED_VARIABLE = YES; 381 | IPHONEOS_DEPLOYMENT_TARGET = 14.4; 382 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 383 | MTL_FAST_MATH = YES; 384 | ONLY_ACTIVE_ARCH = YES; 385 | SDKROOT = iphoneos; 386 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 387 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 388 | }; 389 | name = Debug; 390 | }; 391 | AE599A372663DD7900B6538D /* Release */ = { 392 | isa = XCBuildConfiguration; 393 | buildSettings = { 394 | ALWAYS_SEARCH_USER_PATHS = NO; 395 | CLANG_ANALYZER_NONNULL = YES; 396 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 397 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 398 | CLANG_CXX_LIBRARY = "libc++"; 399 | CLANG_ENABLE_MODULES = YES; 400 | CLANG_ENABLE_OBJC_ARC = YES; 401 | CLANG_ENABLE_OBJC_WEAK = YES; 402 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 403 | CLANG_WARN_BOOL_CONVERSION = YES; 404 | CLANG_WARN_COMMA = YES; 405 | CLANG_WARN_CONSTANT_CONVERSION = YES; 406 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 407 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 408 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 409 | CLANG_WARN_EMPTY_BODY = YES; 410 | CLANG_WARN_ENUM_CONVERSION = YES; 411 | CLANG_WARN_INFINITE_RECURSION = YES; 412 | CLANG_WARN_INT_CONVERSION = YES; 413 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 414 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 415 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 416 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 417 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 418 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 419 | CLANG_WARN_STRICT_PROTOTYPES = YES; 420 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 421 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 422 | CLANG_WARN_UNREACHABLE_CODE = YES; 423 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 424 | COPY_PHASE_STRIP = NO; 425 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 426 | ENABLE_NS_ASSERTIONS = NO; 427 | ENABLE_STRICT_OBJC_MSGSEND = YES; 428 | GCC_C_LANGUAGE_STANDARD = gnu11; 429 | GCC_NO_COMMON_BLOCKS = YES; 430 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 431 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 432 | GCC_WARN_UNDECLARED_SELECTOR = YES; 433 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 434 | GCC_WARN_UNUSED_FUNCTION = YES; 435 | GCC_WARN_UNUSED_VARIABLE = YES; 436 | IPHONEOS_DEPLOYMENT_TARGET = 14.4; 437 | MTL_ENABLE_DEBUG_INFO = NO; 438 | MTL_FAST_MATH = YES; 439 | SDKROOT = iphoneos; 440 | SWIFT_COMPILATION_MODE = wholemodule; 441 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 442 | VALIDATE_PRODUCT = YES; 443 | }; 444 | name = Release; 445 | }; 446 | AE599A392663DD7900B6538D /* Debug */ = { 447 | isa = XCBuildConfiguration; 448 | buildSettings = { 449 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 450 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 451 | CODE_SIGN_STYLE = Automatic; 452 | INFOPLIST_FILE = MaskableImageView/Info.plist; 453 | LD_RUNPATH_SEARCH_PATHS = ( 454 | "$(inherited)", 455 | "@executable_path/Frameworks", 456 | ); 457 | PRODUCT_BUNDLE_IDENTIFIER = com.wareto.MaskableImageView; 458 | PRODUCT_NAME = "$(TARGET_NAME)"; 459 | SWIFT_VERSION = 5.0; 460 | TARGETED_DEVICE_FAMILY = "1,2"; 461 | }; 462 | name = Debug; 463 | }; 464 | AE599A3A2663DD7900B6538D /* Release */ = { 465 | isa = XCBuildConfiguration; 466 | buildSettings = { 467 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 468 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 469 | CODE_SIGN_STYLE = Automatic; 470 | INFOPLIST_FILE = MaskableImageView/Info.plist; 471 | LD_RUNPATH_SEARCH_PATHS = ( 472 | "$(inherited)", 473 | "@executable_path/Frameworks", 474 | ); 475 | PRODUCT_BUNDLE_IDENTIFIER = com.wareto.MaskableImageView; 476 | PRODUCT_NAME = "$(TARGET_NAME)"; 477 | SWIFT_VERSION = 5.0; 478 | TARGETED_DEVICE_FAMILY = "1,2"; 479 | }; 480 | name = Release; 481 | }; 482 | AE599A3C2663DD7900B6538D /* Debug */ = { 483 | isa = XCBuildConfiguration; 484 | buildSettings = { 485 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 486 | BUNDLE_LOADER = "$(TEST_HOST)"; 487 | CODE_SIGN_STYLE = Automatic; 488 | INFOPLIST_FILE = MaskableImageViewTests/Info.plist; 489 | IPHONEOS_DEPLOYMENT_TARGET = 14.4; 490 | LD_RUNPATH_SEARCH_PATHS = ( 491 | "$(inherited)", 492 | "@executable_path/Frameworks", 493 | "@loader_path/Frameworks", 494 | ); 495 | PRODUCT_BUNDLE_IDENTIFIER = com.wareto.MaskableImageViewTests; 496 | PRODUCT_NAME = "$(TARGET_NAME)"; 497 | SWIFT_VERSION = 5.0; 498 | TARGETED_DEVICE_FAMILY = "1,2"; 499 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/MaskableImageView.app/MaskableImageView"; 500 | }; 501 | name = Debug; 502 | }; 503 | AE599A3D2663DD7900B6538D /* Release */ = { 504 | isa = XCBuildConfiguration; 505 | buildSettings = { 506 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 507 | BUNDLE_LOADER = "$(TEST_HOST)"; 508 | CODE_SIGN_STYLE = Automatic; 509 | INFOPLIST_FILE = MaskableImageViewTests/Info.plist; 510 | IPHONEOS_DEPLOYMENT_TARGET = 14.4; 511 | LD_RUNPATH_SEARCH_PATHS = ( 512 | "$(inherited)", 513 | "@executable_path/Frameworks", 514 | "@loader_path/Frameworks", 515 | ); 516 | PRODUCT_BUNDLE_IDENTIFIER = com.wareto.MaskableImageViewTests; 517 | PRODUCT_NAME = "$(TARGET_NAME)"; 518 | SWIFT_VERSION = 5.0; 519 | TARGETED_DEVICE_FAMILY = "1,2"; 520 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/MaskableImageView.app/MaskableImageView"; 521 | }; 522 | name = Release; 523 | }; 524 | AE599A3F2663DD7900B6538D /* Debug */ = { 525 | isa = XCBuildConfiguration; 526 | buildSettings = { 527 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 528 | CODE_SIGN_STYLE = Automatic; 529 | INFOPLIST_FILE = MaskableImageViewUITests/Info.plist; 530 | LD_RUNPATH_SEARCH_PATHS = ( 531 | "$(inherited)", 532 | "@executable_path/Frameworks", 533 | "@loader_path/Frameworks", 534 | ); 535 | PRODUCT_BUNDLE_IDENTIFIER = com.wareto.MaskableImageViewUITests; 536 | PRODUCT_NAME = "$(TARGET_NAME)"; 537 | SWIFT_VERSION = 5.0; 538 | TARGETED_DEVICE_FAMILY = "1,2"; 539 | TEST_TARGET_NAME = MaskableImageView; 540 | }; 541 | name = Debug; 542 | }; 543 | AE599A402663DD7900B6538D /* Release */ = { 544 | isa = XCBuildConfiguration; 545 | buildSettings = { 546 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 547 | CODE_SIGN_STYLE = Automatic; 548 | INFOPLIST_FILE = MaskableImageViewUITests/Info.plist; 549 | LD_RUNPATH_SEARCH_PATHS = ( 550 | "$(inherited)", 551 | "@executable_path/Frameworks", 552 | "@loader_path/Frameworks", 553 | ); 554 | PRODUCT_BUNDLE_IDENTIFIER = com.wareto.MaskableImageViewUITests; 555 | PRODUCT_NAME = "$(TARGET_NAME)"; 556 | SWIFT_VERSION = 5.0; 557 | TARGETED_DEVICE_FAMILY = "1,2"; 558 | TEST_TARGET_NAME = MaskableImageView; 559 | }; 560 | name = Release; 561 | }; 562 | /* End XCBuildConfiguration section */ 563 | 564 | /* Begin XCConfigurationList section */ 565 | AE599A092663DD7500B6538D /* Build configuration list for PBXProject "MaskableImageView" */ = { 566 | isa = XCConfigurationList; 567 | buildConfigurations = ( 568 | AE599A362663DD7900B6538D /* Debug */, 569 | AE599A372663DD7900B6538D /* Release */, 570 | ); 571 | defaultConfigurationIsVisible = 0; 572 | defaultConfigurationName = Release; 573 | }; 574 | AE599A382663DD7900B6538D /* Build configuration list for PBXNativeTarget "MaskableImageView" */ = { 575 | isa = XCConfigurationList; 576 | buildConfigurations = ( 577 | AE599A392663DD7900B6538D /* Debug */, 578 | AE599A3A2663DD7900B6538D /* Release */, 579 | ); 580 | defaultConfigurationIsVisible = 0; 581 | defaultConfigurationName = Release; 582 | }; 583 | AE599A3B2663DD7900B6538D /* Build configuration list for PBXNativeTarget "MaskableImageViewTests" */ = { 584 | isa = XCConfigurationList; 585 | buildConfigurations = ( 586 | AE599A3C2663DD7900B6538D /* Debug */, 587 | AE599A3D2663DD7900B6538D /* Release */, 588 | ); 589 | defaultConfigurationIsVisible = 0; 590 | defaultConfigurationName = Release; 591 | }; 592 | AE599A3E2663DD7900B6538D /* Build configuration list for PBXNativeTarget "MaskableImageViewUITests" */ = { 593 | isa = XCConfigurationList; 594 | buildConfigurations = ( 595 | AE599A3F2663DD7900B6538D /* Debug */, 596 | AE599A402663DD7900B6538D /* Release */, 597 | ); 598 | defaultConfigurationIsVisible = 0; 599 | defaultConfigurationName = Release; 600 | }; 601 | /* End XCConfigurationList section */ 602 | }; 603 | rootObject = AE599A062663DD7500B6538D /* Project object */; 604 | } 605 | --------------------------------------------------------------------------------