├── .gitignore ├── DrawableView.podspec ├── DrawableView.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ └── DrawableView.xcscheme ├── DrawableView ├── Array+Helpers.swift ├── Brush.swift ├── CGPoint+Helpers.swift ├── DrawableView.h ├── DrawableView.swift ├── Info.plist ├── Stroke.swift └── StrokeCollection.swift ├── DrawableViewExample ├── AppDelegate.swift ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Info.plist └── ViewController.swift ├── DrawableViewTests ├── DrawableViewTests.swift ├── Info.plist ├── LatestStrokeCollectionTests.swift ├── StrokeCollectionTests.swift └── StrokeTests.swift ├── LICENSE ├── README.md └── drawing.gif /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | # Package.pins 40 | .build/ 41 | 42 | # CocoaPods 43 | # 44 | # We recommend against adding the Pods directory to your .gitignore. However 45 | # you should judge for yourself, the pros and cons are mentioned at: 46 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 47 | # 48 | # Pods/ 49 | 50 | # Carthage 51 | # 52 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 53 | # Carthage/Checkouts 54 | 55 | Carthage/Build 56 | 57 | # fastlane 58 | # 59 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 60 | # screenshots whenever they are needed. 61 | # For more information about the recommended setup visit: 62 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 63 | 64 | fastlane/report.xml 65 | fastlane/Preview.html 66 | fastlane/screenshots 67 | fastlane/test_output 68 | -------------------------------------------------------------------------------- /DrawableView.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | 3 | s.name = 'DrawableView' 4 | s.version = '0.0.4' 5 | s.summary = 'A UIView subclass that allows the user to draw on it.' 6 | s.homepage = 'https://github.com/EthanSchatzline/DrawableView' 7 | s.license = { type: 'MIT', file: 'LICENSE' } 8 | s.author = { 'ethanschatzline' => 'ethanschatzline@yahoo.com' } 9 | s.social_media_url = 'http://twitter.com/ethanschatzline' 10 | s.platform = :ios, '9.0' 11 | s.source = { :git => 'https://github.com/EthanSchatzline/DrawableView.git', :tag => s.version.to_s } 12 | s.source_files = 'DrawableView/*.swift' 13 | s.frameworks = 'UIKit' 14 | s.swift_version = '4.0' 15 | 16 | end 17 | -------------------------------------------------------------------------------- /DrawableView.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 2E28B3FB1EA462D20055B953 /* Array+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E28B3FA1EA462D20055B953 /* Array+Helpers.swift */; }; 11 | 2E43F2F81EA70DF100709E63 /* StrokeCollectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E43F2F71EA70DF100709E63 /* StrokeCollectionTests.swift */; }; 12 | 2E43F2F91EA70F2600709E63 /* StrokeCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E5FDE1C1E9B237000851B0B /* StrokeCollection.swift */; }; 13 | 2E43F2FA1EA70F3100709E63 /* CGPoint+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EF5FEAD1EA1E48B0091CD82 /* CGPoint+Helpers.swift */; }; 14 | 2E43F2FB1EA70F3400709E63 /* Array+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E28B3FA1EA462D20055B953 /* Array+Helpers.swift */; }; 15 | 2E5FDDB71E9B1ED000851B0B /* DrawableView.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2E5FDDAD1E9B1ED000851B0B /* DrawableView.framework */; }; 16 | 2E5FDDBE1E9B1ED000851B0B /* DrawableView.h in Headers */ = {isa = PBXBuildFile; fileRef = 2E5FDDB01E9B1ED000851B0B /* DrawableView.h */; settings = {ATTRIBUTES = (Public, ); }; }; 17 | 2E5FDE1A1E9B223900851B0B /* DrawableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E5FDE191E9B223900851B0B /* DrawableView.swift */; }; 18 | 2E5FDE1D1E9B237000851B0B /* StrokeCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E5FDE1C1E9B237000851B0B /* StrokeCollection.swift */; }; 19 | 2E5FDE201E9B237A00851B0B /* Stroke.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E5FDE1F1E9B237A00851B0B /* Stroke.swift */; }; 20 | 2E5FDE211E9B237A00851B0B /* Stroke.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E5FDE1F1E9B237A00851B0B /* Stroke.swift */; }; 21 | 2E5FDE231E9B237F00851B0B /* Brush.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E5FDE221E9B237F00851B0B /* Brush.swift */; }; 22 | 2E5FDE241E9B237F00851B0B /* Brush.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E5FDE221E9B237F00851B0B /* Brush.swift */; }; 23 | 2E5FDE341E9B2A6B00851B0B /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E5FDE331E9B2A6B00851B0B /* AppDelegate.swift */; }; 24 | 2E5FDE361E9B2A6B00851B0B /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E5FDE351E9B2A6B00851B0B /* ViewController.swift */; }; 25 | 2E5FDE391E9B2A6B00851B0B /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 2E5FDE371E9B2A6B00851B0B /* Main.storyboard */; }; 26 | 2E5FDE3B1E9B2A6B00851B0B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2E5FDE3A1E9B2A6B00851B0B /* Assets.xcassets */; }; 27 | 2E5FDE3E1E9B2A6B00851B0B /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 2E5FDE3C1E9B2A6B00851B0B /* LaunchScreen.storyboard */; }; 28 | 2EBCA9BE1EA7BCD100DD1789 /* LatestStrokeCollectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EBCA9BD1EA7BCD100DD1789 /* LatestStrokeCollectionTests.swift */; }; 29 | 2EBCA9C01EA88D3E00DD1789 /* DrawableViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EBCA9BF1EA88D3E00DD1789 /* DrawableViewTests.swift */; }; 30 | 2EBCA9C21EA88D5700DD1789 /* StrokeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EBCA9C11EA88D5700DD1789 /* StrokeTests.swift */; }; 31 | 2EC2A6F11E9B2BFA001CC1AC /* DrawableView.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2E5FDDAD1E9B1ED000851B0B /* DrawableView.framework */; }; 32 | 2EC2A6F21E9B2BFA001CC1AC /* DrawableView.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 2E5FDDAD1E9B1ED000851B0B /* DrawableView.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 33 | 2EC8C3B31EAC498E00339859 /* DrawableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E5FDE191E9B223900851B0B /* DrawableView.swift */; }; 34 | 2EF5FEAE1EA1E48B0091CD82 /* CGPoint+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EF5FEAD1EA1E48B0091CD82 /* CGPoint+Helpers.swift */; }; 35 | /* End PBXBuildFile section */ 36 | 37 | /* Begin PBXContainerItemProxy section */ 38 | 2E5FDDB81E9B1ED000851B0B /* PBXContainerItemProxy */ = { 39 | isa = PBXContainerItemProxy; 40 | containerPortal = 2E5FDDA41E9B1ED000851B0B /* Project object */; 41 | proxyType = 1; 42 | remoteGlobalIDString = 2E5FDDAC1E9B1ED000851B0B; 43 | remoteInfo = DrawableView; 44 | }; 45 | 2EC2A6F31E9B2BFA001CC1AC /* PBXContainerItemProxy */ = { 46 | isa = PBXContainerItemProxy; 47 | containerPortal = 2E5FDDA41E9B1ED000851B0B /* Project object */; 48 | proxyType = 1; 49 | remoteGlobalIDString = 2E5FDDAC1E9B1ED000851B0B; 50 | remoteInfo = DrawableView; 51 | }; 52 | /* End PBXContainerItemProxy section */ 53 | 54 | /* Begin PBXCopyFilesBuildPhase section */ 55 | 2EC2A6F51E9B2BFA001CC1AC /* Embed Frameworks */ = { 56 | isa = PBXCopyFilesBuildPhase; 57 | buildActionMask = 2147483647; 58 | dstPath = ""; 59 | dstSubfolderSpec = 10; 60 | files = ( 61 | 2EC2A6F21E9B2BFA001CC1AC /* DrawableView.framework in Embed Frameworks */, 62 | ); 63 | name = "Embed Frameworks"; 64 | runOnlyForDeploymentPostprocessing = 0; 65 | }; 66 | /* End PBXCopyFilesBuildPhase section */ 67 | 68 | /* Begin PBXFileReference section */ 69 | 2E28B3FA1EA462D20055B953 /* Array+Helpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Array+Helpers.swift"; sourceTree = ""; }; 70 | 2E43F2F71EA70DF100709E63 /* StrokeCollectionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StrokeCollectionTests.swift; sourceTree = ""; }; 71 | 2E5FDDAD1E9B1ED000851B0B /* DrawableView.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = DrawableView.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 72 | 2E5FDDB01E9B1ED000851B0B /* DrawableView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DrawableView.h; sourceTree = ""; }; 73 | 2E5FDDB11E9B1ED000851B0B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 74 | 2E5FDDB61E9B1ED000851B0B /* DrawableViewTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DrawableViewTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 75 | 2E5FDDBD1E9B1ED000851B0B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 76 | 2E5FDE191E9B223900851B0B /* DrawableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DrawableView.swift; sourceTree = ""; }; 77 | 2E5FDE1C1E9B237000851B0B /* StrokeCollection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StrokeCollection.swift; sourceTree = ""; }; 78 | 2E5FDE1F1E9B237A00851B0B /* Stroke.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Stroke.swift; sourceTree = ""; }; 79 | 2E5FDE221E9B237F00851B0B /* Brush.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Brush.swift; sourceTree = ""; }; 80 | 2E5FDE311E9B2A6B00851B0B /* DrawableViewExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DrawableViewExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 81 | 2E5FDE331E9B2A6B00851B0B /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 82 | 2E5FDE351E9B2A6B00851B0B /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 83 | 2E5FDE381E9B2A6B00851B0B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 84 | 2E5FDE3A1E9B2A6B00851B0B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 85 | 2E5FDE3D1E9B2A6B00851B0B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 86 | 2E5FDE3F1E9B2A6B00851B0B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 87 | 2EBCA9BD1EA7BCD100DD1789 /* LatestStrokeCollectionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LatestStrokeCollectionTests.swift; sourceTree = ""; }; 88 | 2EBCA9BF1EA88D3E00DD1789 /* DrawableViewTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DrawableViewTests.swift; sourceTree = ""; }; 89 | 2EBCA9C11EA88D5700DD1789 /* StrokeTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StrokeTests.swift; sourceTree = ""; }; 90 | 2EF5FEAD1EA1E48B0091CD82 /* CGPoint+Helpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CGPoint+Helpers.swift"; sourceTree = ""; }; 91 | /* End PBXFileReference section */ 92 | 93 | /* Begin PBXFrameworksBuildPhase section */ 94 | 2E5FDDA91E9B1ED000851B0B /* Frameworks */ = { 95 | isa = PBXFrameworksBuildPhase; 96 | buildActionMask = 2147483647; 97 | files = ( 98 | ); 99 | runOnlyForDeploymentPostprocessing = 0; 100 | }; 101 | 2E5FDDB31E9B1ED000851B0B /* Frameworks */ = { 102 | isa = PBXFrameworksBuildPhase; 103 | buildActionMask = 2147483647; 104 | files = ( 105 | 2E5FDDB71E9B1ED000851B0B /* DrawableView.framework in Frameworks */, 106 | ); 107 | runOnlyForDeploymentPostprocessing = 0; 108 | }; 109 | 2E5FDE2E1E9B2A6B00851B0B /* Frameworks */ = { 110 | isa = PBXFrameworksBuildPhase; 111 | buildActionMask = 2147483647; 112 | files = ( 113 | 2EC2A6F11E9B2BFA001CC1AC /* DrawableView.framework in Frameworks */, 114 | ); 115 | runOnlyForDeploymentPostprocessing = 0; 116 | }; 117 | /* End PBXFrameworksBuildPhase section */ 118 | 119 | /* Begin PBXGroup section */ 120 | 2E28B3F91EA462C10055B953 /* Extensions */ = { 121 | isa = PBXGroup; 122 | children = ( 123 | 2EF5FEAD1EA1E48B0091CD82 /* CGPoint+Helpers.swift */, 124 | 2E28B3FA1EA462D20055B953 /* Array+Helpers.swift */, 125 | ); 126 | name = Extensions; 127 | sourceTree = ""; 128 | }; 129 | 2E5FDDA31E9B1ED000851B0B = { 130 | isa = PBXGroup; 131 | children = ( 132 | 2E5FDE321E9B2A6B00851B0B /* DrawableViewExample */, 133 | 2E5FDDAF1E9B1ED000851B0B /* DrawableView */, 134 | 2E5FDDBA1E9B1ED000851B0B /* DrawableViewTests */, 135 | 2E5FDDAE1E9B1ED000851B0B /* Products */, 136 | ); 137 | sourceTree = ""; 138 | }; 139 | 2E5FDDAE1E9B1ED000851B0B /* Products */ = { 140 | isa = PBXGroup; 141 | children = ( 142 | 2E5FDDAD1E9B1ED000851B0B /* DrawableView.framework */, 143 | 2E5FDDB61E9B1ED000851B0B /* DrawableViewTests.xctest */, 144 | 2E5FDE311E9B2A6B00851B0B /* DrawableViewExample.app */, 145 | ); 146 | name = Products; 147 | sourceTree = ""; 148 | }; 149 | 2E5FDDAF1E9B1ED000851B0B /* DrawableView */ = { 150 | isa = PBXGroup; 151 | children = ( 152 | 2E5FDE191E9B223900851B0B /* DrawableView.swift */, 153 | 2E5FDE251E9B239600851B0B /* BrushAndStroke */, 154 | 2E28B3F91EA462C10055B953 /* Extensions */, 155 | 2E5FDDB01E9B1ED000851B0B /* DrawableView.h */, 156 | 2E5FDDB11E9B1ED000851B0B /* Info.plist */, 157 | ); 158 | path = DrawableView; 159 | sourceTree = ""; 160 | }; 161 | 2E5FDDBA1E9B1ED000851B0B /* DrawableViewTests */ = { 162 | isa = PBXGroup; 163 | children = ( 164 | 2EBCA9BF1EA88D3E00DD1789 /* DrawableViewTests.swift */, 165 | 2EBCA9BD1EA7BCD100DD1789 /* LatestStrokeCollectionTests.swift */, 166 | 2E43F2F71EA70DF100709E63 /* StrokeCollectionTests.swift */, 167 | 2EBCA9C11EA88D5700DD1789 /* StrokeTests.swift */, 168 | 2E5FDDBD1E9B1ED000851B0B /* Info.plist */, 169 | ); 170 | path = DrawableViewTests; 171 | sourceTree = ""; 172 | }; 173 | 2E5FDE251E9B239600851B0B /* BrushAndStroke */ = { 174 | isa = PBXGroup; 175 | children = ( 176 | 2E5FDE221E9B237F00851B0B /* Brush.swift */, 177 | 2E5FDE1F1E9B237A00851B0B /* Stroke.swift */, 178 | 2E5FDE1C1E9B237000851B0B /* StrokeCollection.swift */, 179 | ); 180 | name = BrushAndStroke; 181 | sourceTree = ""; 182 | }; 183 | 2E5FDE321E9B2A6B00851B0B /* DrawableViewExample */ = { 184 | isa = PBXGroup; 185 | children = ( 186 | 2E5FDE331E9B2A6B00851B0B /* AppDelegate.swift */, 187 | 2E5FDE351E9B2A6B00851B0B /* ViewController.swift */, 188 | 2E5FDE371E9B2A6B00851B0B /* Main.storyboard */, 189 | 2E5FDE3A1E9B2A6B00851B0B /* Assets.xcassets */, 190 | 2E5FDE3C1E9B2A6B00851B0B /* LaunchScreen.storyboard */, 191 | 2E5FDE3F1E9B2A6B00851B0B /* Info.plist */, 192 | ); 193 | path = DrawableViewExample; 194 | sourceTree = ""; 195 | }; 196 | /* End PBXGroup section */ 197 | 198 | /* Begin PBXHeadersBuildPhase section */ 199 | 2E5FDDAA1E9B1ED000851B0B /* Headers */ = { 200 | isa = PBXHeadersBuildPhase; 201 | buildActionMask = 2147483647; 202 | files = ( 203 | 2E5FDDBE1E9B1ED000851B0B /* DrawableView.h in Headers */, 204 | ); 205 | runOnlyForDeploymentPostprocessing = 0; 206 | }; 207 | /* End PBXHeadersBuildPhase section */ 208 | 209 | /* Begin PBXNativeTarget section */ 210 | 2E5FDDAC1E9B1ED000851B0B /* DrawableView */ = { 211 | isa = PBXNativeTarget; 212 | buildConfigurationList = 2E5FDDC11E9B1ED000851B0B /* Build configuration list for PBXNativeTarget "DrawableView" */; 213 | buildPhases = ( 214 | 2E5FDDA81E9B1ED000851B0B /* Sources */, 215 | 2E5FDDA91E9B1ED000851B0B /* Frameworks */, 216 | 2E5FDDAA1E9B1ED000851B0B /* Headers */, 217 | 2E5FDDAB1E9B1ED000851B0B /* Resources */, 218 | ); 219 | buildRules = ( 220 | ); 221 | dependencies = ( 222 | ); 223 | name = DrawableView; 224 | productName = DrawableView; 225 | productReference = 2E5FDDAD1E9B1ED000851B0B /* DrawableView.framework */; 226 | productType = "com.apple.product-type.framework"; 227 | }; 228 | 2E5FDDB51E9B1ED000851B0B /* DrawableViewTests */ = { 229 | isa = PBXNativeTarget; 230 | buildConfigurationList = 2E5FDDC41E9B1ED000851B0B /* Build configuration list for PBXNativeTarget "DrawableViewTests" */; 231 | buildPhases = ( 232 | 2E5FDDB21E9B1ED000851B0B /* Sources */, 233 | 2E5FDDB31E9B1ED000851B0B /* Frameworks */, 234 | 2E5FDDB41E9B1ED000851B0B /* Resources */, 235 | ); 236 | buildRules = ( 237 | ); 238 | dependencies = ( 239 | 2E5FDDB91E9B1ED000851B0B /* PBXTargetDependency */, 240 | ); 241 | name = DrawableViewTests; 242 | productName = DrawableViewTests; 243 | productReference = 2E5FDDB61E9B1ED000851B0B /* DrawableViewTests.xctest */; 244 | productType = "com.apple.product-type.bundle.unit-test"; 245 | }; 246 | 2E5FDE301E9B2A6B00851B0B /* DrawableViewExample */ = { 247 | isa = PBXNativeTarget; 248 | buildConfigurationList = 2E5FDE401E9B2A6B00851B0B /* Build configuration list for PBXNativeTarget "DrawableViewExample" */; 249 | buildPhases = ( 250 | 2E5FDE2D1E9B2A6B00851B0B /* Sources */, 251 | 2E5FDE2E1E9B2A6B00851B0B /* Frameworks */, 252 | 2E5FDE2F1E9B2A6B00851B0B /* Resources */, 253 | 2EC2A6F51E9B2BFA001CC1AC /* Embed Frameworks */, 254 | ); 255 | buildRules = ( 256 | ); 257 | dependencies = ( 258 | 2EC2A6F41E9B2BFA001CC1AC /* PBXTargetDependency */, 259 | ); 260 | name = DrawableViewExample; 261 | productName = DrawableViewExample; 262 | productReference = 2E5FDE311E9B2A6B00851B0B /* DrawableViewExample.app */; 263 | productType = "com.apple.product-type.application"; 264 | }; 265 | /* End PBXNativeTarget section */ 266 | 267 | /* Begin PBXProject section */ 268 | 2E5FDDA41E9B1ED000851B0B /* Project object */ = { 269 | isa = PBXProject; 270 | attributes = { 271 | LastSwiftUpdateCheck = 0830; 272 | LastUpgradeCheck = 0830; 273 | ORGANIZATIONNAME = "Ethan Schatzline"; 274 | TargetAttributes = { 275 | 2E5FDDAC1E9B1ED000851B0B = { 276 | CreatedOnToolsVersion = 8.3; 277 | DevelopmentTeam = B8TMU4AK57; 278 | LastSwiftMigration = 0830; 279 | ProvisioningStyle = Automatic; 280 | }; 281 | 2E5FDDB51E9B1ED000851B0B = { 282 | CreatedOnToolsVersion = 8.3; 283 | DevelopmentTeam = B8TMU4AK57; 284 | ProvisioningStyle = Automatic; 285 | }; 286 | 2E5FDE301E9B2A6B00851B0B = { 287 | CreatedOnToolsVersion = 8.3; 288 | DevelopmentTeam = B8TMU4AK57; 289 | ProvisioningStyle = Automatic; 290 | }; 291 | }; 292 | }; 293 | buildConfigurationList = 2E5FDDA71E9B1ED000851B0B /* Build configuration list for PBXProject "DrawableView" */; 294 | compatibilityVersion = "Xcode 3.2"; 295 | developmentRegion = English; 296 | hasScannedForEncodings = 0; 297 | knownRegions = ( 298 | en, 299 | Base, 300 | ); 301 | mainGroup = 2E5FDDA31E9B1ED000851B0B; 302 | productRefGroup = 2E5FDDAE1E9B1ED000851B0B /* Products */; 303 | projectDirPath = ""; 304 | projectRoot = ""; 305 | targets = ( 306 | 2E5FDDAC1E9B1ED000851B0B /* DrawableView */, 307 | 2E5FDDB51E9B1ED000851B0B /* DrawableViewTests */, 308 | 2E5FDE301E9B2A6B00851B0B /* DrawableViewExample */, 309 | ); 310 | }; 311 | /* End PBXProject section */ 312 | 313 | /* Begin PBXResourcesBuildPhase section */ 314 | 2E5FDDAB1E9B1ED000851B0B /* Resources */ = { 315 | isa = PBXResourcesBuildPhase; 316 | buildActionMask = 2147483647; 317 | files = ( 318 | ); 319 | runOnlyForDeploymentPostprocessing = 0; 320 | }; 321 | 2E5FDDB41E9B1ED000851B0B /* Resources */ = { 322 | isa = PBXResourcesBuildPhase; 323 | buildActionMask = 2147483647; 324 | files = ( 325 | ); 326 | runOnlyForDeploymentPostprocessing = 0; 327 | }; 328 | 2E5FDE2F1E9B2A6B00851B0B /* Resources */ = { 329 | isa = PBXResourcesBuildPhase; 330 | buildActionMask = 2147483647; 331 | files = ( 332 | 2E5FDE3E1E9B2A6B00851B0B /* LaunchScreen.storyboard in Resources */, 333 | 2E5FDE3B1E9B2A6B00851B0B /* Assets.xcassets in Resources */, 334 | 2E5FDE391E9B2A6B00851B0B /* Main.storyboard in Resources */, 335 | ); 336 | runOnlyForDeploymentPostprocessing = 0; 337 | }; 338 | /* End PBXResourcesBuildPhase section */ 339 | 340 | /* Begin PBXSourcesBuildPhase section */ 341 | 2E5FDDA81E9B1ED000851B0B /* Sources */ = { 342 | isa = PBXSourcesBuildPhase; 343 | buildActionMask = 2147483647; 344 | files = ( 345 | 2E5FDE1A1E9B223900851B0B /* DrawableView.swift in Sources */, 346 | 2E28B3FB1EA462D20055B953 /* Array+Helpers.swift in Sources */, 347 | 2E5FDE201E9B237A00851B0B /* Stroke.swift in Sources */, 348 | 2E5FDE231E9B237F00851B0B /* Brush.swift in Sources */, 349 | 2EF5FEAE1EA1E48B0091CD82 /* CGPoint+Helpers.swift in Sources */, 350 | 2E5FDE1D1E9B237000851B0B /* StrokeCollection.swift in Sources */, 351 | ); 352 | runOnlyForDeploymentPostprocessing = 0; 353 | }; 354 | 2E5FDDB21E9B1ED000851B0B /* Sources */ = { 355 | isa = PBXSourcesBuildPhase; 356 | buildActionMask = 2147483647; 357 | files = ( 358 | 2E43F2F91EA70F2600709E63 /* StrokeCollection.swift in Sources */, 359 | 2E43F2F81EA70DF100709E63 /* StrokeCollectionTests.swift in Sources */, 360 | 2EC8C3B31EAC498E00339859 /* DrawableView.swift in Sources */, 361 | 2EBCA9C21EA88D5700DD1789 /* StrokeTests.swift in Sources */, 362 | 2EBCA9C01EA88D3E00DD1789 /* DrawableViewTests.swift in Sources */, 363 | 2E43F2FB1EA70F3400709E63 /* Array+Helpers.swift in Sources */, 364 | 2EBCA9BE1EA7BCD100DD1789 /* LatestStrokeCollectionTests.swift in Sources */, 365 | 2E5FDE211E9B237A00851B0B /* Stroke.swift in Sources */, 366 | 2E5FDE241E9B237F00851B0B /* Brush.swift in Sources */, 367 | 2E43F2FA1EA70F3100709E63 /* CGPoint+Helpers.swift in Sources */, 368 | ); 369 | runOnlyForDeploymentPostprocessing = 0; 370 | }; 371 | 2E5FDE2D1E9B2A6B00851B0B /* Sources */ = { 372 | isa = PBXSourcesBuildPhase; 373 | buildActionMask = 2147483647; 374 | files = ( 375 | 2E5FDE361E9B2A6B00851B0B /* ViewController.swift in Sources */, 376 | 2E5FDE341E9B2A6B00851B0B /* AppDelegate.swift in Sources */, 377 | ); 378 | runOnlyForDeploymentPostprocessing = 0; 379 | }; 380 | /* End PBXSourcesBuildPhase section */ 381 | 382 | /* Begin PBXTargetDependency section */ 383 | 2E5FDDB91E9B1ED000851B0B /* PBXTargetDependency */ = { 384 | isa = PBXTargetDependency; 385 | target = 2E5FDDAC1E9B1ED000851B0B /* DrawableView */; 386 | targetProxy = 2E5FDDB81E9B1ED000851B0B /* PBXContainerItemProxy */; 387 | }; 388 | 2EC2A6F41E9B2BFA001CC1AC /* PBXTargetDependency */ = { 389 | isa = PBXTargetDependency; 390 | target = 2E5FDDAC1E9B1ED000851B0B /* DrawableView */; 391 | targetProxy = 2EC2A6F31E9B2BFA001CC1AC /* PBXContainerItemProxy */; 392 | }; 393 | /* End PBXTargetDependency section */ 394 | 395 | /* Begin PBXVariantGroup section */ 396 | 2E5FDE371E9B2A6B00851B0B /* Main.storyboard */ = { 397 | isa = PBXVariantGroup; 398 | children = ( 399 | 2E5FDE381E9B2A6B00851B0B /* Base */, 400 | ); 401 | name = Main.storyboard; 402 | sourceTree = ""; 403 | }; 404 | 2E5FDE3C1E9B2A6B00851B0B /* LaunchScreen.storyboard */ = { 405 | isa = PBXVariantGroup; 406 | children = ( 407 | 2E5FDE3D1E9B2A6B00851B0B /* Base */, 408 | ); 409 | name = LaunchScreen.storyboard; 410 | sourceTree = ""; 411 | }; 412 | /* End PBXVariantGroup section */ 413 | 414 | /* Begin XCBuildConfiguration section */ 415 | 2E5FDDBF1E9B1ED000851B0B /* Debug */ = { 416 | isa = XCBuildConfiguration; 417 | buildSettings = { 418 | ALWAYS_SEARCH_USER_PATHS = NO; 419 | CLANG_ANALYZER_NONNULL = YES; 420 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 421 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 422 | CLANG_CXX_LIBRARY = "libc++"; 423 | CLANG_ENABLE_MODULES = YES; 424 | CLANG_ENABLE_OBJC_ARC = YES; 425 | CLANG_WARN_BOOL_CONVERSION = YES; 426 | CLANG_WARN_CONSTANT_CONVERSION = YES; 427 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 428 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 429 | CLANG_WARN_EMPTY_BODY = YES; 430 | CLANG_WARN_ENUM_CONVERSION = YES; 431 | CLANG_WARN_INFINITE_RECURSION = YES; 432 | CLANG_WARN_INT_CONVERSION = YES; 433 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 434 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 435 | CLANG_WARN_UNREACHABLE_CODE = YES; 436 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 437 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 438 | COPY_PHASE_STRIP = NO; 439 | CURRENT_PROJECT_VERSION = 1; 440 | DEBUG_INFORMATION_FORMAT = dwarf; 441 | ENABLE_STRICT_OBJC_MSGSEND = YES; 442 | ENABLE_TESTABILITY = YES; 443 | GCC_C_LANGUAGE_STANDARD = gnu99; 444 | GCC_DYNAMIC_NO_PIC = NO; 445 | GCC_NO_COMMON_BLOCKS = YES; 446 | GCC_OPTIMIZATION_LEVEL = 0; 447 | GCC_PREPROCESSOR_DEFINITIONS = ( 448 | "DEBUG=1", 449 | "$(inherited)", 450 | ); 451 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 452 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 453 | GCC_WARN_UNDECLARED_SELECTOR = YES; 454 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 455 | GCC_WARN_UNUSED_FUNCTION = YES; 456 | GCC_WARN_UNUSED_VARIABLE = YES; 457 | IPHONEOS_DEPLOYMENT_TARGET = 10.3; 458 | MTL_ENABLE_DEBUG_INFO = YES; 459 | ONLY_ACTIVE_ARCH = YES; 460 | SDKROOT = iphoneos; 461 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 462 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 463 | TARGETED_DEVICE_FAMILY = "1,2"; 464 | VERSIONING_SYSTEM = "apple-generic"; 465 | VERSION_INFO_PREFIX = ""; 466 | }; 467 | name = Debug; 468 | }; 469 | 2E5FDDC01E9B1ED000851B0B /* Release */ = { 470 | isa = XCBuildConfiguration; 471 | buildSettings = { 472 | ALWAYS_SEARCH_USER_PATHS = NO; 473 | CLANG_ANALYZER_NONNULL = YES; 474 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 475 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 476 | CLANG_CXX_LIBRARY = "libc++"; 477 | CLANG_ENABLE_MODULES = YES; 478 | CLANG_ENABLE_OBJC_ARC = YES; 479 | CLANG_WARN_BOOL_CONVERSION = YES; 480 | CLANG_WARN_CONSTANT_CONVERSION = YES; 481 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 482 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 483 | CLANG_WARN_EMPTY_BODY = YES; 484 | CLANG_WARN_ENUM_CONVERSION = YES; 485 | CLANG_WARN_INFINITE_RECURSION = YES; 486 | CLANG_WARN_INT_CONVERSION = YES; 487 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 488 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 489 | CLANG_WARN_UNREACHABLE_CODE = YES; 490 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 491 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 492 | COPY_PHASE_STRIP = NO; 493 | CURRENT_PROJECT_VERSION = 1; 494 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 495 | ENABLE_NS_ASSERTIONS = NO; 496 | ENABLE_STRICT_OBJC_MSGSEND = YES; 497 | GCC_C_LANGUAGE_STANDARD = gnu99; 498 | GCC_NO_COMMON_BLOCKS = YES; 499 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 500 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 501 | GCC_WARN_UNDECLARED_SELECTOR = YES; 502 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 503 | GCC_WARN_UNUSED_FUNCTION = YES; 504 | GCC_WARN_UNUSED_VARIABLE = YES; 505 | IPHONEOS_DEPLOYMENT_TARGET = 10.3; 506 | MTL_ENABLE_DEBUG_INFO = NO; 507 | SDKROOT = iphoneos; 508 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 509 | TARGETED_DEVICE_FAMILY = "1,2"; 510 | VALIDATE_PRODUCT = YES; 511 | VERSIONING_SYSTEM = "apple-generic"; 512 | VERSION_INFO_PREFIX = ""; 513 | }; 514 | name = Release; 515 | }; 516 | 2E5FDDC21E9B1ED000851B0B /* Debug */ = { 517 | isa = XCBuildConfiguration; 518 | buildSettings = { 519 | CLANG_ENABLE_MODULES = YES; 520 | CODE_SIGN_IDENTITY = ""; 521 | DEFINES_MODULE = YES; 522 | DEVELOPMENT_TEAM = B8TMU4AK57; 523 | DYLIB_COMPATIBILITY_VERSION = 1; 524 | DYLIB_CURRENT_VERSION = 1; 525 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 526 | INFOPLIST_FILE = DrawableView/Info.plist; 527 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 528 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 529 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 530 | PRODUCT_BUNDLE_IDENTIFIER = me.ethanschatzline.DrawableView; 531 | PRODUCT_NAME = "$(TARGET_NAME)"; 532 | SKIP_INSTALL = YES; 533 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 534 | SWIFT_VERSION = 4.0; 535 | }; 536 | name = Debug; 537 | }; 538 | 2E5FDDC31E9B1ED000851B0B /* Release */ = { 539 | isa = XCBuildConfiguration; 540 | buildSettings = { 541 | CLANG_ENABLE_MODULES = YES; 542 | CODE_SIGN_IDENTITY = ""; 543 | DEFINES_MODULE = YES; 544 | DEVELOPMENT_TEAM = B8TMU4AK57; 545 | DYLIB_COMPATIBILITY_VERSION = 1; 546 | DYLIB_CURRENT_VERSION = 1; 547 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 548 | INFOPLIST_FILE = DrawableView/Info.plist; 549 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 550 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 551 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 552 | PRODUCT_BUNDLE_IDENTIFIER = me.ethanschatzline.DrawableView; 553 | PRODUCT_NAME = "$(TARGET_NAME)"; 554 | SKIP_INSTALL = YES; 555 | SWIFT_VERSION = 4.0; 556 | }; 557 | name = Release; 558 | }; 559 | 2E5FDDC51E9B1ED000851B0B /* Debug */ = { 560 | isa = XCBuildConfiguration; 561 | buildSettings = { 562 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 563 | DEVELOPMENT_TEAM = B8TMU4AK57; 564 | INFOPLIST_FILE = DrawableViewTests/Info.plist; 565 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 566 | PRODUCT_BUNDLE_IDENTIFIER = me.ethanschatzline.DrawableViewTests; 567 | PRODUCT_NAME = "$(TARGET_NAME)"; 568 | SWIFT_VERSION = 3.0; 569 | }; 570 | name = Debug; 571 | }; 572 | 2E5FDDC61E9B1ED000851B0B /* Release */ = { 573 | isa = XCBuildConfiguration; 574 | buildSettings = { 575 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 576 | DEVELOPMENT_TEAM = B8TMU4AK57; 577 | INFOPLIST_FILE = DrawableViewTests/Info.plist; 578 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 579 | PRODUCT_BUNDLE_IDENTIFIER = me.ethanschatzline.DrawableViewTests; 580 | PRODUCT_NAME = "$(TARGET_NAME)"; 581 | SWIFT_VERSION = 3.0; 582 | }; 583 | name = Release; 584 | }; 585 | 2E5FDE411E9B2A6B00851B0B /* Debug */ = { 586 | isa = XCBuildConfiguration; 587 | buildSettings = { 588 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 589 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 590 | DEVELOPMENT_TEAM = B8TMU4AK57; 591 | INFOPLIST_FILE = DrawableViewExample/Info.plist; 592 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 593 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 594 | PRODUCT_BUNDLE_IDENTIFIER = me.ethanschatzline.DrawableViewExample; 595 | PRODUCT_NAME = "$(TARGET_NAME)"; 596 | SWIFT_VERSION = 3.0; 597 | }; 598 | name = Debug; 599 | }; 600 | 2E5FDE421E9B2A6B00851B0B /* Release */ = { 601 | isa = XCBuildConfiguration; 602 | buildSettings = { 603 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 604 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 605 | DEVELOPMENT_TEAM = B8TMU4AK57; 606 | INFOPLIST_FILE = DrawableViewExample/Info.plist; 607 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 608 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 609 | PRODUCT_BUNDLE_IDENTIFIER = me.ethanschatzline.DrawableViewExample; 610 | PRODUCT_NAME = "$(TARGET_NAME)"; 611 | SWIFT_VERSION = 3.0; 612 | }; 613 | name = Release; 614 | }; 615 | /* End XCBuildConfiguration section */ 616 | 617 | /* Begin XCConfigurationList section */ 618 | 2E5FDDA71E9B1ED000851B0B /* Build configuration list for PBXProject "DrawableView" */ = { 619 | isa = XCConfigurationList; 620 | buildConfigurations = ( 621 | 2E5FDDBF1E9B1ED000851B0B /* Debug */, 622 | 2E5FDDC01E9B1ED000851B0B /* Release */, 623 | ); 624 | defaultConfigurationIsVisible = 0; 625 | defaultConfigurationName = Release; 626 | }; 627 | 2E5FDDC11E9B1ED000851B0B /* Build configuration list for PBXNativeTarget "DrawableView" */ = { 628 | isa = XCConfigurationList; 629 | buildConfigurations = ( 630 | 2E5FDDC21E9B1ED000851B0B /* Debug */, 631 | 2E5FDDC31E9B1ED000851B0B /* Release */, 632 | ); 633 | defaultConfigurationIsVisible = 0; 634 | defaultConfigurationName = Release; 635 | }; 636 | 2E5FDDC41E9B1ED000851B0B /* Build configuration list for PBXNativeTarget "DrawableViewTests" */ = { 637 | isa = XCConfigurationList; 638 | buildConfigurations = ( 639 | 2E5FDDC51E9B1ED000851B0B /* Debug */, 640 | 2E5FDDC61E9B1ED000851B0B /* Release */, 641 | ); 642 | defaultConfigurationIsVisible = 0; 643 | defaultConfigurationName = Release; 644 | }; 645 | 2E5FDE401E9B2A6B00851B0B /* Build configuration list for PBXNativeTarget "DrawableViewExample" */ = { 646 | isa = XCConfigurationList; 647 | buildConfigurations = ( 648 | 2E5FDE411E9B2A6B00851B0B /* Debug */, 649 | 2E5FDE421E9B2A6B00851B0B /* Release */, 650 | ); 651 | defaultConfigurationIsVisible = 0; 652 | defaultConfigurationName = Release; 653 | }; 654 | /* End XCConfigurationList section */ 655 | }; 656 | rootObject = 2E5FDDA41E9B1ED000851B0B /* Project object */; 657 | } 658 | -------------------------------------------------------------------------------- /DrawableView.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /DrawableView.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /DrawableView.xcodeproj/xcshareddata/xcschemes/DrawableView.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 49 | 50 | 51 | 52 | 53 | 54 | 64 | 65 | 71 | 72 | 73 | 74 | 75 | 76 | 82 | 83 | 89 | 90 | 91 | 92 | 94 | 95 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /DrawableView/Array+Helpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FileArray+Helpers.swift 3 | // DrawableView 4 | // 5 | // Created by Ethan Schatzline on 4/16/17. 6 | // Copyright © 2017 Ethan Schatzline. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Array { 12 | subscript (safe index: Int) -> Element? { 13 | return indices ~= index ? self[index] : nil 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /DrawableView/Brush.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Brush.swift 3 | // DrawableView 4 | // 5 | // Created by Ethan Schatzline on 4/9/17. 6 | // Copyright © 2017 Ethan Schatzline. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | struct Brush { 12 | let width: CGFloat 13 | let color: UIColor 14 | let transparency: CGFloat 15 | } 16 | 17 | extension Brush: Equatable {} 18 | func ==(lhs: Brush, rhs: Brush) -> Bool { 19 | let widthEqual = (lhs.width == rhs.width) 20 | let colorEqual = (lhs.color == rhs.color) 21 | let transparencyEqual = (lhs.transparency == rhs.transparency) 22 | return widthEqual && colorEqual && transparencyEqual 23 | } 24 | -------------------------------------------------------------------------------- /DrawableView/CGPoint+Helpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGPoint+Distance.swift 3 | // DrawableView 4 | // 5 | // Created by Ethan Schatzline on 4/14/17. 6 | // Copyright © 2017 Ethan Schatzline. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | extension CGPoint { 13 | func distance(to otherPoint: CGPoint) -> CGFloat { 14 | return sqrt(pow(otherPoint.x - x, 2) + pow(otherPoint.y - y, 2)) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /DrawableView/DrawableView.h: -------------------------------------------------------------------------------- 1 | // 2 | // DrawableView.h 3 | // DrawableView 4 | // 5 | // Created by Ethan Schatzline on 4/9/17. 6 | // Copyright © 2017 Ethan Schatzline. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for DrawableView. 12 | FOUNDATION_EXPORT double DrawableViewVersionNumber; 13 | 14 | //! Project version string for DrawableView. 15 | FOUNDATION_EXPORT const unsigned char DrawableViewVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /DrawableView/DrawableView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DrawableView.swift 3 | // DrawableView 4 | // 5 | // Created by Ethan Schatzline on 4/9/17. 6 | // Copyright © 2017 Ethan Schatzline. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public protocol DrawableViewDelegate: class { 12 | /// Lets the delegate know that the user has begun or ended drawing. 13 | /// 14 | /// - Parameters: 15 | /// - isDrawing: A boolean representing if the user began or ended drawing. 16 | /// 17 | /// - Returns: Void 18 | func setDrawing(_ isDrawing: Bool) 19 | } 20 | 21 | private struct Constants { 22 | static let PointsCountThreshold = 500 23 | } 24 | 25 | private typealias ImageCreationRequestIdentifier = Int 26 | private typealias CreationCallback = (ImageCreationResponse) -> Void 27 | 28 | private struct ImageCreationResponse { 29 | let image: UIImage? 30 | let requestID: ImageCreationRequestIdentifier 31 | } 32 | 33 | public class DrawableView: UIView { 34 | 35 | // MARK: - Public Properties 36 | public weak var delegate: DrawableViewDelegate? 37 | 38 | /// A boolean representing if the DrawableView currently contains a drawing. 39 | public var containsDrawing: Bool { 40 | return !strokes.isEmpty 41 | } 42 | 43 | /// An optional UIImage of the current drawing. 44 | public var image: UIImage? { 45 | UIGraphicsBeginImageContextWithOptions(bounds.size, false, UIScreen.main.scale) 46 | drawHierarchy(in: bounds, afterScreenUpdates: true) 47 | let image = UIGraphicsGetImageFromCurrentImageContext() 48 | UIGraphicsEndImageContext() 49 | return image 50 | } 51 | 52 | /// The width of the current brush. 53 | public var strokeWidth: CGFloat = 4.0 54 | 55 | /// The color of the current brush. 56 | public var strokeColor: UIColor = .red 57 | 58 | /// The transparency of the current brush. 59 | public var strokeTransparency: CGFloat = 1.0 60 | 61 | // MARK: - Private Properties 62 | fileprivate var strokes: StrokeCollection = StrokeCollection() 63 | fileprivate let latestStrokes: LatestStrokeCollection = LatestStrokeCollection() 64 | fileprivate var strokesWaitingForImage: StrokeCollection? 65 | 66 | fileprivate var previousStrokesImage: UIImage? 67 | fileprivate var nextImageCreationRequestId: ImageCreationRequestIdentifier = 0 68 | fileprivate var pendingImageCreationRequestId: ImageCreationRequestIdentifier? 69 | 70 | // Set to true to see the bounding box passed to `setNeedsDisplayIn(rect)` when drawing 71 | fileprivate let isDebugMode: Bool = false 72 | fileprivate var frameView: UIView? 73 | fileprivate var undoWasTapped: Bool = false 74 | 75 | override public func touchesBegan( _ touches: Set, with event: UIEvent?) { 76 | delegate?.setDrawing(true) 77 | if let touch = touches.first { 78 | let point = touch.location(in: self) 79 | let brush = Brush(width: strokeWidth, color: strokeColor, transparency: strokeTransparency) 80 | strokes.newStroke(initialPoint: point, brush: brush) 81 | latestStrokes.newStroke(initialPoint: point, brush: brush) 82 | } 83 | } 84 | 85 | override public func touchesMoved( _ touches: Set, with event: UIEvent?) { 86 | if let touch = touches.first { 87 | drawFromTouch(touch) 88 | } 89 | } 90 | 91 | override public func touchesEnded(_ touches: Set, with event: UIEvent?) { 92 | delegate?.setDrawing(false) 93 | if let touch = touches.first { 94 | drawFromTouch(touch) 95 | } 96 | drawBackBuffer() 97 | } 98 | } 99 | 100 | // MARK: - Undo 101 | public extension DrawableView { 102 | /// Removes the last stroke and re-draws to the screen. 103 | /// 104 | /// - Returns: Void 105 | func undo() { 106 | undoWasTapped = true 107 | strokesWaitingForImage = nil 108 | pendingImageCreationRequestId = nil 109 | 110 | // Remove the last stroke 111 | strokes.removeLastStroke() 112 | latestStrokes.clear() 113 | 114 | // Synchronously create an image from all of the strokes and set it as the "back buffer" image so 115 | // all drawing after this is drawn on top of it 116 | previousStrokesImage = createImage(from: strokes, size: bounds.size) 117 | layer.setNeedsDisplay() 118 | } 119 | 120 | func clear() { 121 | undoWasTapped = false 122 | strokesWaitingForImage = nil 123 | pendingImageCreationRequestId = nil 124 | 125 | // Remove all strokes 126 | strokes.clear() 127 | latestStrokes.clear() 128 | 129 | // Synchronously create an image from all of the strokes and set it as the "back buffer" image so 130 | // all drawing after this is drawn on top of it 131 | previousStrokesImage = createImage(from: strokes, size: bounds.size) 132 | layer.setNeedsDisplay() 133 | } 134 | } 135 | 136 | // MARK: - Drawing 137 | extension DrawableView { 138 | fileprivate func drawFromTouch(_ touch: UITouch) { 139 | let point = touch.location(in: self) 140 | 141 | if let lastStroke = strokes.lastStroke { 142 | // Check if it is over the threshold and force a break in the current stroke 143 | let overThreshold = latestStrokes.transferrablePointCount >= Constants.PointsCountThreshold 144 | if !overThreshold { 145 | strokes.addPoint(point) 146 | latestStrokes.addPoint(point) 147 | } 148 | 149 | redrawLayerInBoundingBox(of: lastStroke) 150 | } 151 | } 152 | 153 | private func redrawLayerInBoundingBox(of stroke: Stroke) { 154 | let pointsToDraw = Array(stroke.points.suffix(3)) 155 | guard let firstPoint = pointsToDraw.first else { return } 156 | 157 | let subPath = CGMutablePath() 158 | var previousPoint = firstPoint 159 | for point in pointsToDraw { 160 | subPath.move(to: previousPoint) 161 | subPath.addLine(to: point) 162 | previousPoint = point 163 | } 164 | 165 | var drawBox = subPath.boundingBox 166 | let brushWidth = stroke.brush.width 167 | drawBox.origin.x -= brushWidth * 0.5 168 | drawBox.origin.y -= brushWidth * 0.5 169 | drawBox.size.width += brushWidth 170 | drawBox.size.height += brushWidth 171 | 172 | if isDebugMode { 173 | frameView?.removeFromSuperview() 174 | let newFrameView = UIView(frame: drawBox) 175 | frameView = newFrameView 176 | newFrameView.backgroundColor = .clear 177 | newFrameView.layer.borderColor = UIColor.black.cgColor 178 | newFrameView.layer.borderWidth = 2 179 | addSubview(newFrameView) 180 | } 181 | 182 | layer.setNeedsDisplay(drawBox) 183 | } 184 | 185 | override public func draw(_ layer: CALayer, in ctx: CGContext) { 186 | guard !strokes.isEmpty else { return } 187 | 188 | if let img = previousStrokesImage?.cgImage { 189 | drawImageFlipped(image: img, in: ctx) 190 | } 191 | 192 | strokesWaitingForImage?.draw(in: ctx) 193 | latestStrokes.draw(in: ctx) 194 | } 195 | 196 | fileprivate func drawBackBuffer() { 197 | undoWasTapped = false 198 | let strokesToMakeImage = latestStrokes.splitInTwo(numPoints: latestStrokes.transferrablePointCount) 199 | let requestID = nextImageCreationRequestId 200 | 201 | // Create a callback that clears appropriate data and updates the "back buffer image" 202 | let imageCreationBlock: CreationCallback = { response in 203 | DispatchQueue.main.async { 204 | if self.undoWasTapped { 205 | self.drawBackBuffer() 206 | return 207 | } 208 | // Check if the request coming back is the latest one we care about 209 | if requestID == response.requestID { 210 | // Clear out the "strokes waiting for image" and "pending request ID" 211 | self.strokesWaitingForImage = nil 212 | self.pendingImageCreationRequestId = nil 213 | self.previousStrokesImage = response.image 214 | } 215 | } 216 | } 217 | 218 | pendingImageCreationRequestId = requestID 219 | strokesWaitingForImage = strokesToMakeImage 220 | nextImageCreationRequestId += 1 221 | 222 | createImageAsynchronously(from: strokesToMakeImage, image: previousStrokesImage, size: bounds.size, requestID: requestID, callback: imageCreationBlock) 223 | } 224 | 225 | fileprivate func drawImageFlipped(image: CGImage, in context: CGContext) { 226 | context.saveGState() 227 | context.translateBy(x: 0.0, y: CGFloat(image.height)) 228 | context.scaleBy(x: 1.0, y: -1.0) 229 | context.draw(image, in: CGRect(x: 0, y: 0, width: CGFloat(image.width), height: CGFloat(image.height))) 230 | context.restoreGState() 231 | } 232 | } 233 | 234 | // MARK: - Image Creation 235 | extension DrawableView { 236 | fileprivate func createImage(from strokes: StrokeCollection, image: UIImage? = nil, size: CGSize) -> UIImage? { 237 | UIGraphicsBeginImageContext(size) 238 | 239 | defer { UIGraphicsEndImageContext() } 240 | guard let context = UIGraphicsGetCurrentContext() else { return nil } 241 | 242 | if let cgImage = image?.cgImage { 243 | drawImageFlipped(image: cgImage, in: context) 244 | } 245 | 246 | strokes.draw(in: context) 247 | return UIGraphicsGetImageFromCurrentImageContext() 248 | } 249 | 250 | fileprivate func createImageAsynchronously(from strokes: StrokeCollection, image: UIImage? = nil, size: CGSize, requestID: ImageCreationRequestIdentifier, callback: @escaping CreationCallback) 251 | { 252 | 253 | DispatchQueue.global(qos: .userInteractive).async { 254 | let image = self.createImage(from: strokes, image: image, size: size) 255 | callback(ImageCreationResponse(image: image, requestID: requestID)) 256 | } 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /DrawableView/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.1 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSPrincipalClass 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /DrawableView/Stroke.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Stroke.swift 3 | // DrawableView 4 | // 5 | // Created by Ethan Schatzline on 4/9/17. 6 | // Copyright © 2017 Ethan Schatzline. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | private struct Constants { 12 | static let BrushWidthThreshold: CGFloat = 16.0 13 | } 14 | 15 | class Stroke { 16 | 17 | var points: [CGPoint] 18 | var brush: Brush 19 | 20 | private var lastSmoothPoints: [CGPoint]? 21 | var smoothPoints: [CGPoint] { 22 | guard points.count > 2 else { return lastSmoothPoints ?? points } 23 | 24 | var smoothPoints: [CGPoint] = lastSmoothPoints ?? [] 25 | for i in 2.. Constants.BrushWidthThreshold) ? 2 : 3 38 | 39 | var t: CGFloat = 0.0 40 | let step: CGFloat = 1.0 / CGFloat(numberOfSegments) 41 | for _ in 0.. Bool { 84 | let brushesEqual = (lhs.brush == rhs.brush) 85 | let pointsEqual = (lhs.points == rhs.points) 86 | let smoothPointsEqual = (lhs.smoothPoints == rhs.smoothPoints) 87 | return brushesEqual && pointsEqual && smoothPointsEqual 88 | } 89 | -------------------------------------------------------------------------------- /DrawableView/StrokeCollection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StrokeCollection.swift 3 | // DrawableView 4 | // 5 | // Created by Ethan Schatzline on 4/9/17. 6 | // Copyright © 2017 Ethan Schatzline. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class StrokeCollection { 12 | 13 | fileprivate var strokes: [Stroke] = [] 14 | private(set) var totalPointCount = 0 15 | 16 | var strokeCount: Int { 17 | return strokes.count 18 | } 19 | 20 | var isEmpty: Bool { 21 | return strokes.isEmpty 22 | } 23 | 24 | var lastStroke: Stroke? { 25 | return strokes.last 26 | } 27 | 28 | var lastPoint: CGPoint? { 29 | return strokes.last?.points.last 30 | } 31 | 32 | var lastBrush: Brush? { 33 | return strokes.last?.brush 34 | } 35 | 36 | fileprivate var lastStrokePointCount: Int { 37 | return strokes.last?.points.count ?? 0 38 | } 39 | 40 | func newStroke(initialPoint: CGPoint, brush: Brush) { 41 | let stroke = Stroke(point: initialPoint, brush: brush) 42 | stroke.points.append(initialPoint) 43 | stroke.points.append(initialPoint) 44 | strokes.append(stroke) 45 | totalPointCount += stroke.points.count 46 | } 47 | 48 | func addPoint(_ point: CGPoint) { 49 | guard let lastStroke = lastStroke else { return } 50 | 51 | if let previousPoint = lastStroke.points.last { 52 | let threshold: CGFloat = 1.5 53 | if previousPoint.distance(to: point) <= threshold { return } 54 | } 55 | 56 | addPointToLastStroke(point, stroke: lastStroke) 57 | } 58 | 59 | fileprivate func addPointToLastStroke(_ point: CGPoint, stroke: Stroke) { 60 | stroke.points.append(point) 61 | totalPointCount += 1 62 | } 63 | 64 | func removeLastStroke() { 65 | if let removedStroke = strokes.popLast() { 66 | totalPointCount -= removedStroke.points.count 67 | } 68 | } 69 | 70 | func draw(in context: CGContext) { 71 | for stroke in strokes { 72 | stroke.drawPath(in: context) 73 | } 74 | } 75 | 76 | func clear() { 77 | strokes.removeAll() 78 | totalPointCount = 0 79 | } 80 | 81 | func splitInTwo(numPoints: Int) -> StrokeCollection { 82 | let newCollection = StrokeCollection() 83 | 84 | // Early exit if empty 85 | guard totalPointCount > 0 else { return newCollection } 86 | 87 | // Can't transfer more than total point count 88 | var pointsLeft = min(numPoints, totalPointCount) 89 | 90 | // Check if more points to transferring 91 | while pointsLeft > 0 { 92 | guard let strokeToTransferFrom = strokes.first else { break } 93 | 94 | // See if transferring the whole stroke 95 | let strokePointCount = strokeToTransferFrom.points.count 96 | if strokePointCount < pointsLeft { 97 | // Just remove the stroke and transfer it to new collection. Decrease the points left and the total point count 98 | strokes.removeFirst() 99 | newCollection.strokes.append(strokeToTransferFrom) 100 | newCollection.totalPointCount += strokePointCount 101 | totalPointCount -= strokePointCount 102 | pointsLeft -= strokePointCount 103 | } else { 104 | // Potentially "splitting" the stroke. 105 | let strokeBeforeSplit = Stroke(point: CGPoint.zero, brush: strokeToTransferFrom.brush) // Initial point doesn't matter 106 | let pointsBeforeSplit = Array(strokeToTransferFrom.points.prefix(pointsLeft)) 107 | 108 | // Have the new stroke collection contain a stroke 109 | // with all of the points before the split 110 | strokeBeforeSplit.points = pointsBeforeSplit 111 | newCollection.strokes.append(strokeBeforeSplit) 112 | 113 | // If the "strokeToTransferFrom" is the last stroke, or if "strokeToTransferFrom" is 114 | // being split in the middle (defined as 'not at the end'), then duplicate the point at 115 | // which the split occurs so that the two paths drawn by the split stroke overlap 116 | let duplicateSplitPoint = (strokes.count == 1) || (pointsLeft < strokePointCount) 117 | let pointsToDrop = pointsLeft - (duplicateSplitPoint ? 1 : 0) // subtract 1 here to duplicate the point of split 118 | let pointsAfterSplit = Array(strokeToTransferFrom.points.dropFirst(pointsToDrop)) 119 | 120 | // Set the new points for the 121 | // strokeToTransferFrom (since it is now split) 122 | strokeToTransferFrom.points = pointsAfterSplit 123 | 124 | // Adjust point counts 125 | newCollection.totalPointCount += pointsLeft 126 | totalPointCount -= pointsToDrop 127 | pointsLeft = 0 128 | } 129 | } 130 | 131 | return newCollection 132 | } 133 | } 134 | 135 | class LatestStrokeCollection: StrokeCollection { 136 | 137 | // A point is considered "transferrable" if 138 | // it is being drawn by an opaque brush 139 | // OR if it is not in the last stroke 140 | private(set) var transferrablePointCount: Int = 0 141 | 142 | private var pointsOfLastStrokeAreTransferrable: Bool { 143 | return (lastBrush?.transparency ?? 0.0) >= 1.0 144 | } 145 | 146 | override func newStroke(initialPoint: CGPoint, brush: Brush) { 147 | // Check if a semi-transprent brush is used for the latest stroke 148 | // if so, those points are now "transferrable" since 149 | // that brush will no longer be the latest stroke 150 | var newTransferrablePoints = !pointsOfLastStrokeAreTransferrable ? lastStrokePointCount : 0 151 | super.newStroke(initialPoint: initialPoint, brush: brush) 152 | 153 | // See if need to include the new stroke's initial point too 154 | // Include if the brush's transparency >= 1.0 155 | newTransferrablePoints += pointsOfLastStrokeAreTransferrable ? lastStrokePointCount : 0 156 | transferrablePointCount += newTransferrablePoints 157 | } 158 | 159 | override fileprivate func addPointToLastStroke(_ point: CGPoint, stroke: Stroke) { 160 | super.addPointToLastStroke(point, stroke: stroke) 161 | 162 | // See if need to include the this point in "transferrable points" 163 | // Include if the brush's transparency >= 1.0 164 | transferrablePointCount += (pointsOfLastStrokeAreTransferrable ? 1 : 0) 165 | } 166 | 167 | override func removeLastStroke() { 168 | transferrablePointCount -= (pointsOfLastStrokeAreTransferrable ? lastStrokePointCount : 0) 169 | super.removeLastStroke() 170 | 171 | // Also, since there is a new "last stroke", need to check 172 | // if the points for the new "last stroke" are transferrable or not. 173 | // If they are not transferrable, then they were included in the transferrablePointCount 174 | // when they were not the "last stroke", so, adjust count for that 175 | transferrablePointCount -= (!pointsOfLastStrokeAreTransferrable ? lastStrokePointCount : 0) 176 | } 177 | 178 | override func clear() { 179 | super.clear() 180 | transferrablePointCount = 0 181 | } 182 | 183 | override func splitInTwo(numPoints: Int) -> StrokeCollection { 184 | let newCollection = super.splitInTwo(numPoints: numPoints) 185 | 186 | // Recalculate the transferrable point count 187 | transferrablePointCount = 0 188 | for stroke in strokes.dropLast() { 189 | transferrablePointCount += stroke.points.count 190 | } 191 | 192 | transferrablePointCount += (pointsOfLastStrokeAreTransferrable ? lastStrokePointCount : 0) 193 | return newCollection 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /DrawableViewExample/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // DrawableViewExample 4 | // 5 | // Created by Ethan Schatzline on 4/9/17. 6 | // Copyright © 2017 Ethan Schatzline. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 17 | return true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /DrawableViewExample/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | } 33 | ], 34 | "info" : { 35 | "version" : 1, 36 | "author" : "xcode" 37 | } 38 | } -------------------------------------------------------------------------------- /DrawableViewExample/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /DrawableViewExample/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 | 29 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 52 | 53 | 54 | 55 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 81 | 90 | 99 | 100 | 101 | 102 | 103 | 104 | 113 | 122 | 131 | 140 | 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 | -------------------------------------------------------------------------------- /DrawableViewExample/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UIRequiresFullScreen 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /DrawableViewExample/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // DrawableViewExample 4 | // 5 | // Created by Ethan Schatzline on 4/9/17. 6 | // Copyright © 2017 Ethan Schatzline. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import DrawableView 11 | 12 | private enum Color { 13 | case red, green, blue, purple, black 14 | 15 | var value: UIColor { 16 | switch self { 17 | case .red: 18 | return .red 19 | case .green: 20 | return .green 21 | case .blue: 22 | return .blue 23 | case .purple: 24 | return .purple 25 | case .black: 26 | return .black 27 | } 28 | } 29 | } 30 | 31 | private enum Width { 32 | case small, medium, large 33 | 34 | var value: CGFloat { 35 | switch self { 36 | case .small: 37 | return 4.0 38 | case .medium: 39 | return 12.0 40 | case .large: 41 | return 24.0 42 | } 43 | } 44 | } 45 | 46 | class ViewController: UIViewController, DrawableViewDelegate { 47 | 48 | @IBOutlet var drawableView: DrawableView! { 49 | didSet { 50 | drawableView.delegate = self 51 | } 52 | } 53 | 54 | @IBOutlet var redButton: UIButton! 55 | @IBOutlet var greenButton: UIButton! 56 | @IBOutlet var blueButton: UIButton! 57 | @IBOutlet var purpleButton: UIButton! 58 | @IBOutlet var blackButton: UIButton! 59 | 60 | @IBOutlet var smallButton: UIButton! 61 | @IBOutlet var mediumButton: UIButton! 62 | @IBOutlet var largeButton: UIButton! 63 | 64 | @IBOutlet var drawingLabel: UILabel! 65 | 66 | override func viewDidLoad() { 67 | super.viewDidLoad() 68 | 69 | setColor(.red) 70 | setWidth(.small) 71 | } 72 | 73 | func setDrawing(_ isDrawing: Bool) { 74 | drawingLabel.text = isDrawing ? "Drawing" : "Not Drawing" 75 | drawingLabel.backgroundColor = isDrawing ? .green : .red 76 | } 77 | 78 | private func color(for bool: Bool) -> UIColor { 79 | return bool ? .lightGray : .clear 80 | } 81 | 82 | fileprivate func setColor(_ color: Color) { 83 | drawableView.strokeColor = color.value 84 | redButton.backgroundColor = self.color(for: (color == .red)) 85 | greenButton.backgroundColor = self.color(for: (color == .green)) 86 | blueButton.backgroundColor = self.color(for: (color == .blue)) 87 | purpleButton.backgroundColor = self.color(for: (color == .purple)) 88 | blackButton.backgroundColor = self.color(for: (color == .black)) 89 | } 90 | 91 | fileprivate func setWidth(_ width: Width) { 92 | drawableView.strokeWidth = width.value 93 | smallButton.backgroundColor = self.color(for: (width == .small)) 94 | mediumButton.backgroundColor = self.color(for: (width == .medium)) 95 | largeButton.backgroundColor = self.color(for: (width == .large)) 96 | } 97 | } 98 | 99 | extension ViewController { 100 | @IBAction func transparencySwitchDidChange(_ sender: UISwitch) { 101 | drawableView.strokeTransparency = sender.isOn ? 0.5 : 1.0 102 | } 103 | 104 | @IBAction func undoButtonTapped(_ sender: Any) { 105 | drawableView.undo() 106 | } 107 | } 108 | 109 | extension ViewController { 110 | @IBAction func smallButtonTapped(_ sender: Any) { 111 | setWidth(.small) 112 | } 113 | 114 | @IBAction func mediumButtonTapped(_ sender: Any) { 115 | setWidth(.medium) 116 | } 117 | 118 | @IBAction func largeButtonTapped(_ sender: Any) { 119 | setWidth(.large) 120 | } 121 | } 122 | 123 | extension ViewController { 124 | @IBAction func redButtonTapped(_ sender: Any) { 125 | setColor(.red) 126 | } 127 | 128 | @IBAction func greenButtonTapped(_ sender: Any) { 129 | setColor(.green) 130 | } 131 | 132 | @IBAction func blueButtonTapped(_ sender: Any) { 133 | setColor(.blue) 134 | } 135 | 136 | @IBAction func purpleButtonTapped(_ sender: Any) { 137 | setColor(.purple) 138 | } 139 | 140 | @IBAction func blackButtonTapped(_ sender: Any) { 141 | setColor(.black) 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /DrawableViewTests/DrawableViewTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DrawableViewTests.swift 3 | // DrawableView 4 | // 5 | // Created by Ethan Schatzline on 4/19/17. 6 | // Copyright © 2017 Ethan Schatzline. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | class DrawableViewTests: XCTestCase { 12 | 13 | var drawableView: DrawableView! 14 | let blackBrush: Brush = Brush(width: 2.0, color: .black, transparency: 1.0) 15 | 16 | override func setUp() { 17 | super.setUp() 18 | 19 | drawableView = DrawableView() 20 | } 21 | 22 | override func tearDown() { 23 | super.tearDown() 24 | } 25 | 26 | // Nothing to test in DrawableView yet 27 | } 28 | -------------------------------------------------------------------------------- /DrawableViewTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /DrawableViewTests/LatestStrokeCollectionTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LatestStrokeCollectionTests.swift 3 | // DrawableView 4 | // 5 | // Created by Ethan Schatzline on 4/19/17. 6 | // Copyright © 2017 Ethan Schatzline. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | class LatestStrokeCollectionTests: XCTestCase { 12 | 13 | var latestStrokeCollection: LatestStrokeCollection! 14 | let blackBrush: Brush = Brush(width: 2.0, color: .black, transparency: 1.0) 15 | 16 | override func setUp() { 17 | super.setUp() 18 | 19 | latestStrokeCollection = LatestStrokeCollection() 20 | testIsEmptyStrokeCollection() 21 | } 22 | 23 | override func tearDown() { 24 | super.tearDown() 25 | 26 | latestStrokeCollection.clear() 27 | } 28 | 29 | func testIsEmptyStrokeCollection() { 30 | XCTAssertNil(latestStrokeCollection.lastStroke) 31 | XCTAssertNil(latestStrokeCollection.lastBrush) 32 | XCTAssertNil(latestStrokeCollection.lastPoint) 33 | XCTAssertEqual(latestStrokeCollection.totalPointCount, 0) 34 | XCTAssertEqual(latestStrokeCollection.strokeCount, 0) 35 | XCTAssertTrue(latestStrokeCollection.isEmpty) 36 | XCTAssertEqual(latestStrokeCollection.transferrablePointCount, 0) 37 | } 38 | 39 | func testNewStroke() { 40 | let transparentBrush = Brush(width: 4.0, color: .red, transparency: 0.5) 41 | latestStrokeCollection.newStroke(initialPoint: .zero, brush: transparentBrush) 42 | XCTAssertEqual(latestStrokeCollection.transferrablePointCount, 0) 43 | latestStrokeCollection.newStroke(initialPoint: .zero, brush: blackBrush) 44 | XCTAssertEqual(latestStrokeCollection.transferrablePointCount, 6) 45 | } 46 | 47 | func testAddPoint() { 48 | latestStrokeCollection.newStroke(initialPoint: .zero, brush: blackBrush) 49 | let point = CGPoint(x: 10, y: 10) 50 | latestStrokeCollection.addPoint(point) 51 | // Create a point with a distance exactly equal to 1.5 from the point we just added 52 | // So that it is NOT added 53 | let secondPoint = CGPoint(x: 11.060660171779821, y: 11.060660171779821) 54 | latestStrokeCollection.addPoint(secondPoint) 55 | XCTAssertEqual(latestStrokeCollection.transferrablePointCount, 4) 56 | } 57 | 58 | func testRemoveLastStroke() { 59 | latestStrokeCollection.newStroke(initialPoint: .zero, brush: blackBrush) 60 | latestStrokeCollection.removeLastStroke() 61 | testIsEmptyStrokeCollection() 62 | } 63 | 64 | func testClear() { 65 | latestStrokeCollection.newStroke(initialPoint: .zero, brush: blackBrush) 66 | latestStrokeCollection.newStroke(initialPoint: .zero, brush: blackBrush) 67 | latestStrokeCollection.clear() 68 | testIsEmptyStrokeCollection() 69 | } 70 | 71 | func testSplitInTwoIsEmpty() { 72 | let emptyCollection = latestStrokeCollection.splitInTwo(numPoints: 1000) 73 | XCTAssertNil(emptyCollection.lastStroke) 74 | XCTAssertNil(emptyCollection.lastBrush) 75 | XCTAssertNil(emptyCollection.lastPoint) 76 | XCTAssertEqual(emptyCollection.totalPointCount, 0) 77 | XCTAssertEqual(emptyCollection.strokeCount, 0) 78 | XCTAssertTrue(emptyCollection.isEmpty) 79 | } 80 | 81 | func testSplitInTwoTransfersStroke() { 82 | latestStrokeCollection.newStroke(initialPoint: .zero, brush: blackBrush) 83 | latestStrokeCollection.newStroke(initialPoint: .zero, brush: blackBrush) 84 | let newCollection = latestStrokeCollection.splitInTwo(numPoints: 500) 85 | XCTAssertEqual(latestStrokeCollection.totalPointCount, 1) 86 | XCTAssertEqual(latestStrokeCollection.transferrablePointCount, 1) 87 | XCTAssertEqual(newCollection.totalPointCount, 6) 88 | } 89 | 90 | func testSplitInTwoSplitsStroke() { 91 | latestStrokeCollection.newStroke(initialPoint: .zero, brush: blackBrush) 92 | let newCollection = latestStrokeCollection.splitInTwo(numPoints: 2) 93 | XCTAssertEqual(latestStrokeCollection.transferrablePointCount, 2) 94 | XCTAssertEqual(latestStrokeCollection.totalPointCount, 2) 95 | XCTAssertEqual(newCollection.totalPointCount, 2) 96 | XCTAssertEqual(latestStrokeCollection.strokeCount, 1) 97 | XCTAssertEqual(newCollection.strokeCount, 1) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /DrawableViewTests/StrokeCollectionTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StrokeCollectionTests.swift 3 | // DrawableView 4 | // 5 | // Created by Ethan Schatzline on 4/18/17. 6 | // Copyright © 2017 Ethan Schatzline. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | class StrokeCollectionTests: XCTestCase { 12 | 13 | var strokeCollection: StrokeCollection! 14 | let blackBrush: Brush = Brush(width: 2.0, color: .black, transparency: 1.0) 15 | 16 | override func setUp() { 17 | super.setUp() 18 | 19 | strokeCollection = StrokeCollection() 20 | testIsEmptyStrokeCollection() 21 | } 22 | 23 | override func tearDown() { 24 | super.tearDown() 25 | 26 | strokeCollection.clear() 27 | } 28 | 29 | func testIsEmptyStrokeCollection() { 30 | XCTAssertNil(strokeCollection.lastStroke) 31 | XCTAssertNil(strokeCollection.lastBrush) 32 | XCTAssertNil(strokeCollection.lastPoint) 33 | XCTAssertEqual(strokeCollection.totalPointCount, 0) 34 | XCTAssertEqual(strokeCollection.strokeCount, 0) 35 | XCTAssertTrue(strokeCollection.isEmpty) 36 | } 37 | 38 | func testTotalPointCount() { 39 | strokeCollection.newStroke(initialPoint: .zero, brush: blackBrush) 40 | strokeCollection.newStroke(initialPoint: .zero, brush: blackBrush) 41 | strokeCollection.removeLastStroke() 42 | XCTAssertEqual(strokeCollection.totalPointCount, 3) 43 | } 44 | 45 | func testStrokeCount() { 46 | strokeCollection.newStroke(initialPoint: .zero, brush: blackBrush) 47 | XCTAssertEqual(strokeCollection.strokeCount, 1) 48 | } 49 | 50 | func testIsEmpty() { 51 | strokeCollection.newStroke(initialPoint: .zero, brush: blackBrush) 52 | XCTAssertFalse(strokeCollection.isEmpty) 53 | strokeCollection.removeLastStroke() 54 | XCTAssertTrue(strokeCollection.isEmpty) 55 | } 56 | 57 | func testLastStroke() { 58 | strokeCollection.newStroke(initialPoint: .zero, brush: blackBrush) 59 | let stroke = Stroke(point: .zero, brush: blackBrush) 60 | stroke.points.append(.zero) 61 | stroke.points.append(.zero) 62 | XCTAssertEqual(strokeCollection.lastStroke, stroke) 63 | } 64 | 65 | func testLastPoint() { 66 | strokeCollection.newStroke(initialPoint: .zero, brush: blackBrush) 67 | XCTAssertEqual(strokeCollection.lastPoint, .zero) 68 | } 69 | 70 | func testLastBrush() { 71 | strokeCollection.newStroke(initialPoint: .zero, brush: blackBrush) 72 | XCTAssertEqual(strokeCollection.lastBrush, blackBrush) 73 | } 74 | 75 | func testNewStroke() { 76 | strokeCollection.newStroke(initialPoint: .zero, brush: blackBrush) 77 | XCTAssertEqual(strokeCollection.strokeCount, 1) 78 | } 79 | 80 | func testNewStrokeAppendsToEnd() { 81 | strokeCollection.newStroke(initialPoint: .zero, brush: blackBrush) 82 | let secondPoint = CGPoint(x: 5, y: 5) 83 | strokeCollection.newStroke(initialPoint: secondPoint, brush: blackBrush) 84 | // Create a stroke equal to the one we just added 85 | let stroke = Stroke(point: secondPoint, brush: blackBrush) 86 | stroke.points.append(secondPoint) 87 | stroke.points.append(secondPoint) 88 | XCTAssertEqual(strokeCollection.lastStroke, stroke) 89 | } 90 | 91 | func testAddPointToEmptyCollection() { 92 | strokeCollection.addPoint(.zero) 93 | testIsEmptyStrokeCollection() 94 | } 95 | 96 | func testAddInvalidPoint() { 97 | strokeCollection.newStroke(initialPoint: .zero, brush: blackBrush) 98 | let point = CGPoint(x: 1.060660171779821, y: 1.060660171779821) 99 | strokeCollection.addPoint(point) 100 | XCTAssertEqual(strokeCollection.totalPointCount, 3) 101 | } 102 | 103 | func testAddPoint() { 104 | strokeCollection.newStroke(initialPoint: .zero, brush: blackBrush) 105 | let point = CGPoint(x: 10, y: 10) 106 | strokeCollection.addPoint(point) 107 | XCTAssertEqual(strokeCollection.totalPointCount, 4) 108 | } 109 | 110 | func testRemoveLastStroke() { 111 | strokeCollection.newStroke(initialPoint: .zero, brush: blackBrush) 112 | strokeCollection.removeLastStroke() 113 | testIsEmptyStrokeCollection() 114 | } 115 | 116 | func testClear() { 117 | strokeCollection.newStroke(initialPoint: .zero, brush: blackBrush) 118 | strokeCollection.newStroke(initialPoint: .zero, brush: blackBrush) 119 | strokeCollection.clear() 120 | testIsEmptyStrokeCollection() 121 | } 122 | 123 | func testSplitInTwoIsEmpty() { 124 | let emptyCollection = strokeCollection.splitInTwo(numPoints: 1000) 125 | XCTAssertNil(emptyCollection.lastStroke) 126 | XCTAssertNil(emptyCollection.lastBrush) 127 | XCTAssertNil(emptyCollection.lastPoint) 128 | XCTAssertEqual(emptyCollection.totalPointCount, 0) 129 | XCTAssertEqual(emptyCollection.strokeCount, 0) 130 | XCTAssertTrue(emptyCollection.isEmpty) 131 | } 132 | 133 | func testSplitInTwoTransfersStroke() { 134 | strokeCollection.newStroke(initialPoint: .zero, brush: blackBrush) 135 | strokeCollection.newStroke(initialPoint: .zero, brush: blackBrush) 136 | let newCollection = strokeCollection.splitInTwo(numPoints: 500) 137 | XCTAssertEqual(strokeCollection.totalPointCount, 1) 138 | XCTAssertEqual(newCollection.totalPointCount, 6) 139 | } 140 | 141 | func testSplitInTwoSplitsStroke() { 142 | strokeCollection.newStroke(initialPoint: .zero, brush: blackBrush) 143 | let newCollection = strokeCollection.splitInTwo(numPoints: 2) 144 | XCTAssertEqual(strokeCollection.totalPointCount, 2) 145 | XCTAssertEqual(newCollection.totalPointCount, 2) 146 | XCTAssertEqual(strokeCollection.strokeCount, 1) 147 | XCTAssertEqual(newCollection.strokeCount, 1) 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /DrawableViewTests/StrokeTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StrokeTests.swift 3 | // DrawableView 4 | // 5 | // Created by Ethan Schatzline on 4/19/17. 6 | // Copyright © 2017 Ethan Schatzline. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | class StrokeTests: XCTestCase { 12 | 13 | let blackBrush: Brush = Brush(width: 2.0, color: .black, transparency: 1.0) 14 | var stroke: Stroke! 15 | 16 | override func setUp() { 17 | super.setUp() 18 | 19 | stroke = Stroke(point: .zero, brush: blackBrush) 20 | stroke.points.append(.zero) 21 | stroke.points.append(.zero) 22 | } 23 | 24 | override func tearDown() { 25 | super.tearDown() 26 | 27 | stroke.points.removeAll() 28 | } 29 | 30 | func testSmoothPoints() { 31 | stroke.points.append(CGPoint(x: 10, y: 10)) 32 | stroke.points.append(CGPoint(x: 0, y: 20)) 33 | // Expected points for stroke.smoothPoints 34 | // Interpolated points minus the last 2 points ^ 35 | let expectedPoints = [ 36 | CGPoint(x: 0, y: 0), 37 | CGPoint(x: 0, y: 0), 38 | CGPoint(x: 0, y: 0), 39 | CGPoint(x: 0, y: 0), 40 | CGPoint(x: 0, y: 0), 41 | CGPoint(x: 0.55555555555555558, y: 0.55555555555555558), 42 | CGPoint(x: 2.2222222222222223, y: 2.2222222222222223), 43 | CGPoint(x: 5, y: 5), 44 | CGPoint(x: 5, y: 5), 45 | CGPoint(x: 7.2222222222222232, y: 8.3333333333333339), 46 | CGPoint(x: 7.2222222222222223, y: 11.666666666666666), 47 | CGPoint(x: 5, y: 15) 48 | ] 49 | XCTAssertEqual(stroke.smoothPoints, expectedPoints) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Ethan Schatzline 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DrawableView 2 | ## A UIView subclass that allows the user to draw on it. 3 | 4 | [![Swift Version][swift-image]][swift-url] 5 | [![License][license-image]][license-url] 6 | [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) 7 | [![CocoaPods Compatible](https://img.shields.io/cocoapods/v/DrawableView.svg)](https://img.shields.io/cocoapods/v/DrawableView.svg) 8 | [![Platform](https://img.shields.io/cocoapods/p/DrawableView.svg?style=flat)](http://cocoapods.org/pods/DrawableView) 9 | [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) 10 | 11 | Add a DrawableView to your app and you will immediately be able to draw on it. Then try changing the stroke color, width, and transparency! 12 | 13 | 14 | 15 | ## Features 16 | 17 | - [x] Stroke Color 18 | - [x] Stroke Width 19 | - [x] Stroke Transparency 20 | - [x] Undo 21 | - [x] DrawableViewDelegate 22 | - [x] Quad Curve Interpolation 23 | 24 | ## Requirements 25 | 26 | - iOS 9.0+ 27 | - Xcode 8.3.1 28 | 29 | ## Installation 30 | 31 | #### CocoaPods 32 | You can use [CocoaPods](http://cocoapods.org/) to install `DrawableView` by adding it to your `Podfile`: 33 | 34 | ```ruby 35 | pod 'DrawableView' 36 | ``` 37 | 38 | Simply import `DrawableView` wherever you would like to use it. 39 | 40 | ``` swift 41 | import UIKit 42 | import DrawableView 43 | ``` 44 | #### Carthage 45 | Create a `Cartfile` that lists the framework and run `carthage update`. Follow the [instructions](https://github.com/Carthage/Carthage#if-youre-building-for-ios) to add `$(SRCROOT)/Carthage/Build/iOS/DrawableView.framework` to an iOS project. 46 | 47 | ``` 48 | github "EthanSchatzline/DrawableView" 49 | ``` 50 | 51 | ## Usage example 52 | 53 | ```swift 54 | class ViewController: UIViewController, DrawableViewDelegate { 55 | 56 | @IBOutlet var drawableView: DrawableView! { 57 | didSet { 58 | drawableView.delegate = self 59 | drawableView.strokeColor = .blue 60 | drawableView.strokeWidth = 12.0 61 | drawableView.transparency = 1.0 62 | } 63 | } 64 | 65 | func setDrawing(_ isDrawing: Bool) { 66 | /* 67 | Run some logic based on if the user is currently drawing a stroke or not. 68 | Commonly people hide the drawing tools UI while the user is drawing and fade it back in once they stop. 69 | */ 70 | } 71 | 72 | func saveDrawingToPhotoLibrary() { 73 | guard let drawnImage = drawableView.image else { return } 74 | UIImageWriteToSavedPhotosAlbum(drawnImage, self, nil, nil) 75 | } 76 | } 77 | ``` 78 | 79 | ## Contribute 80 | 81 | We would love for you to contribute to **DrawableView**, check the ``LICENSE`` file for more info. 82 | 83 | ## Meta 84 | 85 | Ethan Schatzline – [@_Easy_E](https://twitter.com/_easy_e) – ethanschatzline@gmail.com 86 | 87 | Distributed under the MIT license. See ``LICENSE`` for more information. 88 | 89 | [https://github.com/EthansShatzline/](https://github.com/EthanSchatzline/) 90 | 91 | [swift-image]:https://img.shields.io/badge/swift-4-orange.svg 92 | [swift-url]: https://swift.org/ 93 | [license-image]: https://img.shields.io/badge/License-MIT-blue.svg 94 | [license-url]: LICENSE 95 | [travis-image]: https://img.shields.io/travis/dbader/node-datadog-metrics/master.svg?style=flat-square 96 | [travis-url]: https://travis-ci.org/dbader/node-datadog-metrics 97 | [codebeat-image]: https://codebeat.co/badges/c19b47ea-2f9d-45df-8458-b2d952fe9dad 98 | [codebeat-url]: https://codebeat.co/projects/github-com-vsouza-awesomeios-com 99 | -------------------------------------------------------------------------------- /drawing.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EthanSchatzline/DrawableView/86aaab1b6aad72f4366d24df53a6ada95ea7fa0f/drawing.gif --------------------------------------------------------------------------------