├── 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/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 |
--------------------------------------------------------------------------------