├── .gitignore ├── BTSPieChart.xcodeproj ├── project.pbxproj └── xcshareddata │ └── xcschemes │ └── BTSPieChart.xcscheme ├── BTSPieChart ├── BTSAppDelegate.h ├── BTSAppDelegate.m ├── BTSDemoViewController.h ├── BTSDemoViewController.m ├── BTSPieChart-Info.plist ├── BTSPieChart-Prefix.pch ├── BTSPieLayer.h ├── BTSPieLayer.m ├── BTSPieView.h ├── BTSPieView.mm ├── BTSPieViewController.h ├── BTSPieViewController.m ├── BTSPieViewValues.h ├── BTSSliceLayer.h ├── BTSSliceLayer.m ├── MainStoryboard.storyboard ├── en.lproj │ └── InfoPlist.strings └── main.m └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | profile 3 | 4 | 5 | *.mode1 6 | *.mode1v3 7 | *.mode2v3 8 | *.perspective 9 | *.perspectivev3 10 | *.pbxuser 11 | 12 | # Xcode 4 13 | xcuserdata/ 14 | project.xcworkspace/ 15 | 16 | # automatic backup files 17 | *~.nib 18 | *.swp 19 | 20 | .idea 21 | -------------------------------------------------------------------------------- /BTSPieChart.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 5D28120A14494C8500D248AC /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5D28120914494C8500D248AC /* UIKit.framework */; }; 11 | 5D28120C14494C8500D248AC /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5D28120B14494C8500D248AC /* Foundation.framework */; }; 12 | 5D28120E14494C8500D248AC /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5D28120D14494C8500D248AC /* CoreGraphics.framework */; }; 13 | 5D28121414494C8500D248AC /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5D28121214494C8500D248AC /* InfoPlist.strings */; }; 14 | 5D28121614494C8500D248AC /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 5D28121514494C8500D248AC /* main.m */; }; 15 | 5D28122E14494CC800D248AC /* BTSAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 5D28122714494CC800D248AC /* BTSAppDelegate.m */; }; 16 | 5D28122F14494CC800D248AC /* BTSDemoViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 5D28122914494CC800D248AC /* BTSDemoViewController.m */; }; 17 | 5D28123114494CC800D248AC /* BTSPieView.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5D28122D14494CC800D248AC /* BTSPieView.mm */; }; 18 | 5D28123314494CED00D248AC /* MainStoryboard.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 5D28123214494CED00D248AC /* MainStoryboard.storyboard */; }; 19 | 5D28123514494D1700D248AC /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5D28123414494D1700D248AC /* QuartzCore.framework */; }; 20 | 5D28123B14494E2E00D248AC /* readme.md in Resources */ = {isa = PBXBuildFile; fileRef = 5D28123A14494E2E00D248AC /* readme.md */; }; 21 | 5D3E81AF14D8F42F0036FBD9 /* BTSPieViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 5D3E81AE14D8F42F0036FBD9 /* BTSPieViewController.m */; }; 22 | 5D599DF014DDC33D00C41E19 /* BTSPieLayer.m in Sources */ = {isa = PBXBuildFile; fileRef = 5D599DEF14DDC33D00C41E19 /* BTSPieLayer.m */; }; 23 | 5D599DF314DDC3B200C41E19 /* BTSSliceLayer.m in Sources */ = {isa = PBXBuildFile; fileRef = 5D599DF214DDC3B200C41E19 /* BTSSliceLayer.m */; }; 24 | /* End PBXBuildFile section */ 25 | 26 | /* Begin PBXFileReference section */ 27 | 5D28120514494C8500D248AC /* BTSPieChart.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = BTSPieChart.app; sourceTree = BUILT_PRODUCTS_DIR; }; 28 | 5D28120914494C8500D248AC /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; 29 | 5D28120B14494C8500D248AC /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; 30 | 5D28120D14494C8500D248AC /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; 31 | 5D28121114494C8500D248AC /* BTSPieChart-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "BTSPieChart-Info.plist"; sourceTree = ""; }; 32 | 5D28121314494C8500D248AC /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 33 | 5D28121514494C8500D248AC /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 34 | 5D28121714494C8500D248AC /* BTSPieChart-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "BTSPieChart-Prefix.pch"; sourceTree = ""; }; 35 | 5D28122614494CC800D248AC /* BTSAppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BTSAppDelegate.h; sourceTree = ""; }; 36 | 5D28122714494CC800D248AC /* BTSAppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BTSAppDelegate.m; sourceTree = ""; }; 37 | 5D28122814494CC800D248AC /* BTSDemoViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BTSDemoViewController.h; sourceTree = ""; }; 38 | 5D28122914494CC800D248AC /* BTSDemoViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BTSDemoViewController.m; sourceTree = ""; }; 39 | 5D28122C14494CC800D248AC /* BTSPieView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BTSPieView.h; sourceTree = ""; }; 40 | 5D28122D14494CC800D248AC /* BTSPieView.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = BTSPieView.mm; sourceTree = ""; }; 41 | 5D28123214494CED00D248AC /* MainStoryboard.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = MainStoryboard.storyboard; sourceTree = ""; }; 42 | 5D28123414494D1700D248AC /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; 43 | 5D28123A14494E2E00D248AC /* readme.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = readme.md; sourceTree = ""; }; 44 | 5D3E81AD14D8F42F0036FBD9 /* BTSPieViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BTSPieViewController.h; sourceTree = ""; }; 45 | 5D3E81AE14D8F42F0036FBD9 /* BTSPieViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BTSPieViewController.m; sourceTree = ""; }; 46 | 5D599DEE14DDC33D00C41E19 /* BTSPieLayer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BTSPieLayer.h; sourceTree = ""; }; 47 | 5D599DEF14DDC33D00C41E19 /* BTSPieLayer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BTSPieLayer.m; sourceTree = ""; }; 48 | 5D599DF114DDC3B200C41E19 /* BTSSliceLayer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BTSSliceLayer.h; sourceTree = ""; }; 49 | 5D599DF214DDC3B200C41E19 /* BTSSliceLayer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BTSSliceLayer.m; sourceTree = ""; }; 50 | 5DA2CA1A14D8DC51006738C7 /* BTSPieViewValues.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BTSPieViewValues.h; sourceTree = ""; }; 51 | /* End PBXFileReference section */ 52 | 53 | /* Begin PBXFrameworksBuildPhase section */ 54 | 5D28120214494C8500D248AC /* Frameworks */ = { 55 | isa = PBXFrameworksBuildPhase; 56 | buildActionMask = 2147483647; 57 | files = ( 58 | 5D28123514494D1700D248AC /* QuartzCore.framework in Frameworks */, 59 | 5D28120A14494C8500D248AC /* UIKit.framework in Frameworks */, 60 | 5D28120C14494C8500D248AC /* Foundation.framework in Frameworks */, 61 | 5D28120E14494C8500D248AC /* CoreGraphics.framework in Frameworks */, 62 | ); 63 | runOnlyForDeploymentPostprocessing = 0; 64 | }; 65 | /* End PBXFrameworksBuildPhase section */ 66 | 67 | /* Begin PBXGroup section */ 68 | 5D2811FA14494C8500D248AC = { 69 | isa = PBXGroup; 70 | children = ( 71 | 5D28120F14494C8500D248AC /* BTSPieChart */, 72 | 5D28120814494C8500D248AC /* Frameworks */, 73 | 5D28120614494C8500D248AC /* Products */, 74 | 5D28123A14494E2E00D248AC /* readme.md */, 75 | ); 76 | sourceTree = ""; 77 | }; 78 | 5D28120614494C8500D248AC /* Products */ = { 79 | isa = PBXGroup; 80 | children = ( 81 | 5D28120514494C8500D248AC /* BTSPieChart.app */, 82 | ); 83 | name = Products; 84 | sourceTree = ""; 85 | }; 86 | 5D28120814494C8500D248AC /* Frameworks */ = { 87 | isa = PBXGroup; 88 | children = ( 89 | 5D28123414494D1700D248AC /* QuartzCore.framework */, 90 | 5D28120914494C8500D248AC /* UIKit.framework */, 91 | 5D28120B14494C8500D248AC /* Foundation.framework */, 92 | 5D28120D14494C8500D248AC /* CoreGraphics.framework */, 93 | ); 94 | name = Frameworks; 95 | sourceTree = ""; 96 | }; 97 | 5D28120F14494C8500D248AC /* BTSPieChart */ = { 98 | isa = PBXGroup; 99 | children = ( 100 | 5D599DFD14DDC8FD00C41E19 /* Demo App */, 101 | 5D599DFC14DDC8DF00C41E19 /* View and Layers */, 102 | 5D28121014494C8500D248AC /* Supporting Files */, 103 | ); 104 | path = BTSPieChart; 105 | sourceTree = ""; 106 | }; 107 | 5D28121014494C8500D248AC /* Supporting Files */ = { 108 | isa = PBXGroup; 109 | children = ( 110 | 5D28121114494C8500D248AC /* BTSPieChart-Info.plist */, 111 | 5D28121214494C8500D248AC /* InfoPlist.strings */, 112 | 5D28121514494C8500D248AC /* main.m */, 113 | 5D28121714494C8500D248AC /* BTSPieChart-Prefix.pch */, 114 | ); 115 | name = "Supporting Files"; 116 | sourceTree = ""; 117 | }; 118 | 5D599DFC14DDC8DF00C41E19 /* View and Layers */ = { 119 | isa = PBXGroup; 120 | children = ( 121 | 5DA2CA1A14D8DC51006738C7 /* BTSPieViewValues.h */, 122 | 5D28122C14494CC800D248AC /* BTSPieView.h */, 123 | 5D28122D14494CC800D248AC /* BTSPieView.mm */, 124 | 5D599DEE14DDC33D00C41E19 /* BTSPieLayer.h */, 125 | 5D599DEF14DDC33D00C41E19 /* BTSPieLayer.m */, 126 | 5D599DF114DDC3B200C41E19 /* BTSSliceLayer.h */, 127 | 5D599DF214DDC3B200C41E19 /* BTSSliceLayer.m */, 128 | ); 129 | name = "View and Layers"; 130 | sourceTree = ""; 131 | }; 132 | 5D599DFD14DDC8FD00C41E19 /* Demo App */ = { 133 | isa = PBXGroup; 134 | children = ( 135 | 5D28123214494CED00D248AC /* MainStoryboard.storyboard */, 136 | 5D28122614494CC800D248AC /* BTSAppDelegate.h */, 137 | 5D28122714494CC800D248AC /* BTSAppDelegate.m */, 138 | 5D28122814494CC800D248AC /* BTSDemoViewController.h */, 139 | 5D28122914494CC800D248AC /* BTSDemoViewController.m */, 140 | 5D3E81AD14D8F42F0036FBD9 /* BTSPieViewController.h */, 141 | 5D3E81AE14D8F42F0036FBD9 /* BTSPieViewController.m */, 142 | ); 143 | name = "Demo App"; 144 | sourceTree = ""; 145 | }; 146 | /* End PBXGroup section */ 147 | 148 | /* Begin PBXNativeTarget section */ 149 | 5D28120414494C8500D248AC /* BTSPieChart */ = { 150 | isa = PBXNativeTarget; 151 | buildConfigurationList = 5D28122314494C8500D248AC /* Build configuration list for PBXNativeTarget "BTSPieChart" */; 152 | buildPhases = ( 153 | 5D28120114494C8500D248AC /* Sources */, 154 | 5D28120214494C8500D248AC /* Frameworks */, 155 | 5D28120314494C8500D248AC /* Resources */, 156 | ); 157 | buildRules = ( 158 | ); 159 | dependencies = ( 160 | ); 161 | name = BTSPieChart; 162 | productName = BTSPieChart; 163 | productReference = 5D28120514494C8500D248AC /* BTSPieChart.app */; 164 | productType = "com.apple.product-type.application"; 165 | }; 166 | /* End PBXNativeTarget section */ 167 | 168 | /* Begin PBXProject section */ 169 | 5D2811FC14494C8500D248AC /* Project object */ = { 170 | isa = PBXProject; 171 | attributes = { 172 | LastUpgradeCheck = 1000; 173 | ORGANIZATIONNAME = "Brian Coyner"; 174 | TargetAttributes = { 175 | 5D28120414494C8500D248AC = { 176 | DevelopmentTeam = KC5B683642; 177 | }; 178 | }; 179 | }; 180 | buildConfigurationList = 5D2811FF14494C8500D248AC /* Build configuration list for PBXProject "BTSPieChart" */; 181 | compatibilityVersion = "Xcode 3.2"; 182 | developmentRegion = English; 183 | hasScannedForEncodings = 0; 184 | knownRegions = ( 185 | en, 186 | ); 187 | mainGroup = 5D2811FA14494C8500D248AC; 188 | productRefGroup = 5D28120614494C8500D248AC /* Products */; 189 | projectDirPath = ""; 190 | projectRoot = ""; 191 | targets = ( 192 | 5D28120414494C8500D248AC /* BTSPieChart */, 193 | ); 194 | }; 195 | /* End PBXProject section */ 196 | 197 | /* Begin PBXResourcesBuildPhase section */ 198 | 5D28120314494C8500D248AC /* Resources */ = { 199 | isa = PBXResourcesBuildPhase; 200 | buildActionMask = 2147483647; 201 | files = ( 202 | 5D28121414494C8500D248AC /* InfoPlist.strings in Resources */, 203 | 5D28123314494CED00D248AC /* MainStoryboard.storyboard in Resources */, 204 | 5D28123B14494E2E00D248AC /* readme.md in Resources */, 205 | ); 206 | runOnlyForDeploymentPostprocessing = 0; 207 | }; 208 | /* End PBXResourcesBuildPhase section */ 209 | 210 | /* Begin PBXSourcesBuildPhase section */ 211 | 5D28120114494C8500D248AC /* Sources */ = { 212 | isa = PBXSourcesBuildPhase; 213 | buildActionMask = 2147483647; 214 | files = ( 215 | 5D28121614494C8500D248AC /* main.m in Sources */, 216 | 5D28122E14494CC800D248AC /* BTSAppDelegate.m in Sources */, 217 | 5D28122F14494CC800D248AC /* BTSDemoViewController.m in Sources */, 218 | 5D28123114494CC800D248AC /* BTSPieView.mm in Sources */, 219 | 5D3E81AF14D8F42F0036FBD9 /* BTSPieViewController.m in Sources */, 220 | 5D599DF014DDC33D00C41E19 /* BTSPieLayer.m in Sources */, 221 | 5D599DF314DDC3B200C41E19 /* BTSSliceLayer.m in Sources */, 222 | ); 223 | runOnlyForDeploymentPostprocessing = 0; 224 | }; 225 | /* End PBXSourcesBuildPhase section */ 226 | 227 | /* Begin PBXVariantGroup section */ 228 | 5D28121214494C8500D248AC /* InfoPlist.strings */ = { 229 | isa = PBXVariantGroup; 230 | children = ( 231 | 5D28121314494C8500D248AC /* en */, 232 | ); 233 | name = InfoPlist.strings; 234 | sourceTree = ""; 235 | }; 236 | /* End PBXVariantGroup section */ 237 | 238 | /* Begin XCBuildConfiguration section */ 239 | 5D28122114494C8500D248AC /* Debug */ = { 240 | isa = XCBuildConfiguration; 241 | buildSettings = { 242 | ALWAYS_SEARCH_USER_PATHS = NO; 243 | CLANG_ENABLE_OBJC_ARC = YES; 244 | CLANG_STATIC_ANALYZER_MODE = deep; 245 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 246 | CLANG_WARN_BOOL_CONVERSION = YES; 247 | CLANG_WARN_COMMA = YES; 248 | CLANG_WARN_CONSTANT_CONVERSION = YES; 249 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 250 | CLANG_WARN_EMPTY_BODY = YES; 251 | CLANG_WARN_ENUM_CONVERSION = YES; 252 | CLANG_WARN_INFINITE_RECURSION = YES; 253 | CLANG_WARN_INT_CONVERSION = YES; 254 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 255 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 256 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 257 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 258 | CLANG_WARN_STRICT_PROTOTYPES = YES; 259 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 260 | CLANG_WARN_UNREACHABLE_CODE = YES; 261 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 262 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 263 | COPY_PHASE_STRIP = NO; 264 | ENABLE_STRICT_OBJC_MSGSEND = YES; 265 | ENABLE_TESTABILITY = YES; 266 | GCC_C_LANGUAGE_STANDARD = "compiler-default"; 267 | GCC_DYNAMIC_NO_PIC = NO; 268 | GCC_NO_COMMON_BLOCKS = YES; 269 | GCC_OPTIMIZATION_LEVEL = 0; 270 | GCC_PREPROCESSOR_DEFINITIONS = ( 271 | "DEBUG=1", 272 | "$(inherited)", 273 | ); 274 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 275 | GCC_TREAT_WARNINGS_AS_ERRORS = YES; 276 | GCC_VERSION = com.apple.compilers.llvm.clang.1_0; 277 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 278 | GCC_WARN_ABOUT_MISSING_NEWLINE = YES; 279 | GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES; 280 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 281 | GCC_WARN_UNDECLARED_SELECTOR = YES; 282 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 283 | GCC_WARN_UNUSED_FUNCTION = YES; 284 | GCC_WARN_UNUSED_VARIABLE = YES; 285 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 286 | ONLY_ACTIVE_ARCH = YES; 287 | RUN_CLANG_STATIC_ANALYZER = YES; 288 | SDKROOT = iphoneos; 289 | TARGETED_DEVICE_FAMILY = 2; 290 | }; 291 | name = Debug; 292 | }; 293 | 5D28122214494C8500D248AC /* Release */ = { 294 | isa = XCBuildConfiguration; 295 | buildSettings = { 296 | ALWAYS_SEARCH_USER_PATHS = NO; 297 | CLANG_ENABLE_OBJC_ARC = YES; 298 | CLANG_STATIC_ANALYZER_MODE = deep; 299 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 300 | CLANG_WARN_BOOL_CONVERSION = YES; 301 | CLANG_WARN_COMMA = YES; 302 | CLANG_WARN_CONSTANT_CONVERSION = YES; 303 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 304 | CLANG_WARN_EMPTY_BODY = YES; 305 | CLANG_WARN_ENUM_CONVERSION = YES; 306 | CLANG_WARN_INFINITE_RECURSION = YES; 307 | CLANG_WARN_INT_CONVERSION = YES; 308 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 309 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 310 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 311 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 312 | CLANG_WARN_STRICT_PROTOTYPES = YES; 313 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 314 | CLANG_WARN_UNREACHABLE_CODE = YES; 315 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 316 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 317 | COPY_PHASE_STRIP = YES; 318 | ENABLE_STRICT_OBJC_MSGSEND = YES; 319 | GCC_C_LANGUAGE_STANDARD = "compiler-default"; 320 | GCC_NO_COMMON_BLOCKS = YES; 321 | GCC_TREAT_WARNINGS_AS_ERRORS = YES; 322 | GCC_VERSION = com.apple.compilers.llvm.clang.1_0; 323 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 324 | GCC_WARN_ABOUT_MISSING_NEWLINE = YES; 325 | GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES; 326 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 327 | GCC_WARN_UNDECLARED_SELECTOR = YES; 328 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 329 | GCC_WARN_UNUSED_FUNCTION = YES; 330 | GCC_WARN_UNUSED_VARIABLE = YES; 331 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 332 | OTHER_CFLAGS = "-DNS_BLOCK_ASSERTIONS=1"; 333 | RUN_CLANG_STATIC_ANALYZER = YES; 334 | SDKROOT = iphoneos; 335 | TARGETED_DEVICE_FAMILY = 2; 336 | VALIDATE_PRODUCT = YES; 337 | }; 338 | name = Release; 339 | }; 340 | 5D28122414494C8500D248AC /* Debug */ = { 341 | isa = XCBuildConfiguration; 342 | buildSettings = { 343 | CLANG_WARN_IMPLICIT_SIGN_CONVERSION = NO; 344 | DEVELOPMENT_TEAM = KC5B683642; 345 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 346 | GCC_PREFIX_HEADER = "BTSPieChart/BTSPieChart-Prefix.pch"; 347 | GCC_TREAT_WARNINGS_AS_ERRORS = YES; 348 | GCC_WARN_NON_VIRTUAL_DESTRUCTOR = YES; 349 | GCC_WARN_SHADOW = YES; 350 | GCC_WARN_UNDECLARED_SELECTOR = YES; 351 | GCC_WARN_UNKNOWN_PRAGMAS = YES; 352 | GCC_WARN_UNUSED_LABEL = YES; 353 | INFOPLIST_FILE = "BTSPieChart/BTSPieChart-Info.plist"; 354 | PRODUCT_BUNDLE_IDENTIFIER = "com.briancoyner.${PRODUCT_NAME:rfc1034identifier}"; 355 | PRODUCT_NAME = "$(TARGET_NAME)"; 356 | RUN_CLANG_STATIC_ANALYZER = YES; 357 | WRAPPER_EXTENSION = app; 358 | }; 359 | name = Debug; 360 | }; 361 | 5D28122514494C8500D248AC /* Release */ = { 362 | isa = XCBuildConfiguration; 363 | buildSettings = { 364 | CLANG_WARN_IMPLICIT_SIGN_CONVERSION = NO; 365 | DEVELOPMENT_TEAM = KC5B683642; 366 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 367 | GCC_PREFIX_HEADER = "BTSPieChart/BTSPieChart-Prefix.pch"; 368 | GCC_TREAT_WARNINGS_AS_ERRORS = YES; 369 | GCC_WARN_NON_VIRTUAL_DESTRUCTOR = YES; 370 | GCC_WARN_SHADOW = YES; 371 | GCC_WARN_UNDECLARED_SELECTOR = YES; 372 | GCC_WARN_UNKNOWN_PRAGMAS = YES; 373 | GCC_WARN_UNUSED_LABEL = YES; 374 | INFOPLIST_FILE = "BTSPieChart/BTSPieChart-Info.plist"; 375 | PRODUCT_BUNDLE_IDENTIFIER = "com.briancoyner.${PRODUCT_NAME:rfc1034identifier}"; 376 | PRODUCT_NAME = "$(TARGET_NAME)"; 377 | RUN_CLANG_STATIC_ANALYZER = YES; 378 | WRAPPER_EXTENSION = app; 379 | }; 380 | name = Release; 381 | }; 382 | /* End XCBuildConfiguration section */ 383 | 384 | /* Begin XCConfigurationList section */ 385 | 5D2811FF14494C8500D248AC /* Build configuration list for PBXProject "BTSPieChart" */ = { 386 | isa = XCConfigurationList; 387 | buildConfigurations = ( 388 | 5D28122114494C8500D248AC /* Debug */, 389 | 5D28122214494C8500D248AC /* Release */, 390 | ); 391 | defaultConfigurationIsVisible = 0; 392 | defaultConfigurationName = Release; 393 | }; 394 | 5D28122314494C8500D248AC /* Build configuration list for PBXNativeTarget "BTSPieChart" */ = { 395 | isa = XCConfigurationList; 396 | buildConfigurations = ( 397 | 5D28122414494C8500D248AC /* Debug */, 398 | 5D28122514494C8500D248AC /* Release */, 399 | ); 400 | defaultConfigurationIsVisible = 0; 401 | defaultConfigurationName = Release; 402 | }; 403 | /* End XCConfigurationList section */ 404 | }; 405 | rootObject = 5D2811FC14494C8500D248AC /* Project object */; 406 | } 407 | -------------------------------------------------------------------------------- /BTSPieChart.xcodeproj/xcshareddata/xcschemes/BTSPieChart.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /BTSPieChart/BTSAppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // BTSAppDelegate.h 3 | // 4 | // Copyright (c) 2011 Brian Coyner. All rights reserved. 5 | // 6 | 7 | #import 8 | 9 | @interface BTSAppDelegate : UIResponder 10 | 11 | @end 12 | -------------------------------------------------------------------------------- /BTSPieChart/BTSAppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // BTSAppDelegate.m 3 | // 4 | // Copyright (c) 2011 Brian Coyner. All rights reserved. 5 | // 6 | 7 | #import "BTSAppDelegate.h" 8 | 9 | #import "BTSDemoViewController.h" 10 | #import "BTSPieViewController.h" 11 | 12 | @implementation BTSAppDelegate 13 | 14 | @synthesize window = _window; 15 | 16 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 17 | { 18 | UISplitViewController *splitViewController = (UISplitViewController *) [[self window] rootViewController]; 19 | 20 | BTSPieViewController *pieViewController = [[splitViewController viewControllers] lastObject]; 21 | BTSPieView *pieView = [pieViewController pieView]; 22 | 23 | UINavigationController *navigationController = [[splitViewController viewControllers] objectAtIndex:0]; 24 | BTSDemoViewController *demoViewController = (BTSDemoViewController *) [navigationController topViewController]; 25 | [demoViewController setPieView:pieView]; 26 | 27 | [splitViewController setPreferredDisplayMode:UISplitViewControllerDisplayModeAllVisible]; 28 | 29 | return YES; 30 | } 31 | 32 | @end 33 | -------------------------------------------------------------------------------- /BTSPieChart/BTSDemoViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // BTSViewController.h 3 | // 4 | // Copyright (c) 2011 Brian Coyner. All rights reserved. 5 | // 6 | 7 | #import 8 | 9 | #import "BTSPieView.h" 10 | 11 | @interface BTSDemoViewController : UITableViewController 12 | 13 | @property (nonatomic, weak, readwrite) BTSPieView *pieView; 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /BTSPieChart/BTSDemoViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // BTSViewController.m 3 | // 4 | // Copyright (c) 2011 Brian Coyner. All rights reserved. 5 | // 6 | 7 | #import "BTSDemoViewController.h" 8 | 9 | #import 10 | 11 | @interface BTSSliceData : NSObject 12 | 13 | @property (nonatomic) int value; 14 | @property (nonatomic, strong) UIColor *color; 15 | 16 | + (id)sliceDataWithValue:(int)value color:(UIColor *)color; 17 | 18 | @end 19 | 20 | // 21 | // This is a very simple view controller used to display and control a BTSPieView chart view. 22 | // 23 | // NOTE: This view controller restricts various interactions with the pie view. 24 | // Specifically, there must be a valid selection to delete a pie wedge. The selection 25 | // is cleared after every deletion. This keeps the user from pressing the "-" button 26 | // really fast, which causes issues with this version of the BTSPieView. 27 | // 28 | // Please see BTSPieChart.m for additional notes. 29 | 30 | @interface BTSDemoViewController () { 31 | 32 | NSMutableArray *_slices; 33 | NSInteger _selectedSliceIndex; 34 | 35 | NSArray *_availableSliceColors; 36 | NSInteger _nextColorIndex; 37 | 38 | __weak IBOutlet UIStepper *_sliceStepper; 39 | __weak IBOutlet UISwitch *_toggleAnimationSwitch; 40 | __weak IBOutlet UISlider *_selectedSliceValueSlider; 41 | __weak IBOutlet UILabel *_selectedSliceValueLabel; 42 | __weak IBOutlet UISlider *_animationSpeedSlider; 43 | __weak IBOutlet UILabel *_animationDurationLabel; 44 | } 45 | 46 | @end 47 | 48 | @implementation BTSDemoViewController 49 | 50 | @synthesize pieView = _pieView; 51 | 52 | #pragma mark - View Life Cycle 53 | 54 | - (void)viewDidLoad 55 | { 56 | [super viewDidLoad]; 57 | 58 | // initialize the user interface with reasonable defaults 59 | [_animationSpeedSlider setValue:0.5]; 60 | [self updateAnimationSpeed:_animationSpeedSlider]; 61 | 62 | [_selectedSliceValueSlider setValue:0.0]; 63 | [_selectedSliceValueSlider setEnabled:NO]; 64 | [_selectedSliceValueLabel setAlpha:0.0]; 65 | [self updateSelectedSliceValue:_selectedSliceValueSlider]; 66 | 67 | [_sliceStepper setValue:0]; 68 | [self updateSliceCount:_sliceStepper]; 69 | 70 | // start with a blank slate 71 | _slices = [[NSMutableArray alloc] init]; 72 | _selectedSliceIndex = -1; 73 | 74 | // set up the data source and delegate 75 | [_pieView setDataSource:self]; 76 | [_pieView setDelegate:self]; 77 | 78 | _availableSliceColors = @[ 79 | [UIColor colorWithRed:93.0f / 255.0f green:150.0f / 255.0f blue:72.0f / 255.0f alpha:1.0f], 80 | [UIColor colorWithRed:46.0f / 255.0f green:87.0f / 255.0f blue:140.0f / 255.0f alpha:1.0f], 81 | [UIColor colorWithRed:231.0f / 255.0f green:161.0f / 255.0f blue:61.0f / 255.0f alpha:1.0f], 82 | [UIColor colorWithRed:188.0f / 255.0f green:45.0f / 255.0f blue:48.0f / 255.0f alpha:1.0f], 83 | [UIColor colorWithRed:111.0f / 255.0f green:61.0f / 255.0f blue:121.0f / 255.0f alpha:1.0f], 84 | [UIColor colorWithRed:125.0f / 255.0f green:128.0f / 255.0f blue:127.0f / 255.0f alpha:1.0f], 85 | ]; 86 | _nextColorIndex = -1; 87 | 88 | [_pieView reloadData]; 89 | } 90 | 91 | #pragma mark - BTSPieView Data Source 92 | 93 | - (NSUInteger)numberOfSlicesInPieView:(BTSPieView *)pieView 94 | { 95 | return [_slices count]; 96 | } 97 | 98 | - (CGFloat)pieView:(BTSPieView *)pieView valueForSliceAtIndex:(NSUInteger)index 99 | { 100 | return [(BTSSliceData *) [_slices objectAtIndex:index] value]; 101 | } 102 | 103 | - (UIColor *)pieView:(BTSPieView *)pieView colorForSliceAtIndex:(NSUInteger)index sliceCount:(NSUInteger)sliceCount 104 | { 105 | return [(BTSSliceData *) [_slices objectAtIndex:index] color]; 106 | } 107 | 108 | #pragma mark - BTSPieView Delegate 109 | 110 | - (void)pieView:(BTSPieView *)pieView willSelectSliceAtIndex:(NSInteger)index 111 | { 112 | } 113 | 114 | - (void)pieView:(BTSPieView *)pieView didSelectSliceAtIndex:(NSInteger)index 115 | { 116 | // save the index the user selected. 117 | _selectedSliceIndex = index; 118 | 119 | // update the selected slice UI components with the model values 120 | BTSSliceData *sliceData = [_slices objectAtIndex:(NSUInteger) _selectedSliceIndex]; 121 | [_selectedSliceValueLabel setText:[NSString stringWithFormat:@"%d", [sliceData value]]]; 122 | [_selectedSliceValueLabel setAlpha:1.0]; 123 | 124 | [_selectedSliceValueSlider setValue:[sliceData value]]; 125 | [_selectedSliceValueSlider setEnabled:YES]; 126 | [_selectedSliceValueSlider setMinimumTrackTintColor:[sliceData color]]; 127 | [_selectedSliceValueSlider setMaximumTrackTintColor:[sliceData color]]; 128 | } 129 | 130 | - (void)pieView:(BTSPieView *)pieView willDeselectSliceAtIndex:(NSInteger)index 131 | { 132 | } 133 | 134 | - (void)pieView:(BTSPieView *)pieView didDeselectSliceAtIndex:(NSInteger)index 135 | { 136 | [_selectedSliceValueSlider setMinimumTrackTintColor:nil]; 137 | [_selectedSliceValueSlider setMaximumTrackTintColor:nil]; 138 | 139 | // nothing is selected... so turn off the "selected value" controls 140 | _selectedSliceIndex = -1; 141 | [_selectedSliceValueSlider setEnabled:NO]; 142 | [_selectedSliceValueSlider setValue:0.0]; 143 | [_selectedSliceValueLabel setAlpha:0.0]; 144 | 145 | [self updateSelectedSliceValue:_selectedSliceValueSlider]; 146 | } 147 | 148 | #pragma mark - Value Manipulation 149 | 150 | - (IBAction)updateSliceCount:(id)sender 151 | { 152 | UIStepper *stepper = (UIStepper *) sender; 153 | NSUInteger sliceCount = (NSUInteger) [stepper value]; 154 | BOOL shouldAnimate = [_toggleAnimationSwitch isOn]; 155 | 156 | if ([_slices count] < sliceCount) { // "+" pressed 157 | 158 | NSUInteger insertIndex = (NSUInteger) _selectedSliceIndex + 1; 159 | 160 | _nextColorIndex = _nextColorIndex + 1 < [_availableSliceColors count] ? _nextColorIndex + 1 : 0; 161 | UIColor *sliceColor = [_availableSliceColors objectAtIndex:(NSUInteger) _nextColorIndex]; 162 | 163 | BTSSliceData *sliceData = [BTSSliceData sliceDataWithValue:10 color:sliceColor]; 164 | [_slices insertObject:sliceData atIndex:insertIndex]; 165 | 166 | [_pieView insertSliceAtIndex:insertIndex animate:shouldAnimate]; 167 | } else if ([_slices count] > sliceCount) { // "-" pressed 168 | 169 | // The user wants to remove the selected layer. We only allow the user to remove a selected layer 170 | // if there is a known selection. 171 | if (_selectedSliceIndex > -1) { 172 | 173 | [_slices removeObjectAtIndex:(NSUInteger) _selectedSliceIndex]; 174 | [_pieView removeSliceAtIndex:(NSUInteger) _selectedSliceIndex animate:shouldAnimate]; 175 | 176 | // As mentioned in the class level notes, any time a wedge is deleted the view controller's 177 | // selection index is set to -1 (no selection). This keeps the user from pressing the "-" 178 | // stepper button really fast and causing the pie view to go nuts. Yes, this is a problem 179 | // with this version of the BTSPieView. 180 | _selectedSliceIndex = -1; 181 | } else { 182 | 183 | // no selection... reset the stepper... no need to reload the pie view. 184 | [_sliceStepper setValue:sliceCount + 1]; 185 | [self updateSliceCount:_sliceStepper]; 186 | } 187 | } 188 | } 189 | 190 | - (IBAction)updateAnimationSpeed:(id)sender 191 | { 192 | UISlider *slider = (UISlider *) sender; 193 | float animationDuration = [slider value]; 194 | [_animationDurationLabel setText:[NSString stringWithFormat:@"%0.1f", animationDuration]]; 195 | [_pieView setAnimationDuration:animationDuration]; 196 | } 197 | 198 | - (IBAction)updateSelectedSliceValue:(id)sender 199 | { 200 | int value = (int) [_selectedSliceValueSlider value]; 201 | [_selectedSliceValueLabel setText:[NSString stringWithFormat:@"%d", value]]; 202 | 203 | if (_selectedSliceIndex != -1) { 204 | 205 | BOOL shouldAnimate = [_toggleAnimationSwitch isOn]; 206 | 207 | BTSSliceData *sliceData = [_slices objectAtIndex:(NSUInteger) _selectedSliceIndex]; 208 | [sliceData setValue:value]; 209 | 210 | [_pieView reloadSliceAtIndex:(NSUInteger) _selectedSliceIndex animate:shouldAnimate]; 211 | } 212 | } 213 | 214 | #pragma mark - Toggle Layer Visibility 215 | 216 | - (IBAction)toggleLineLayers:(id)sender 217 | { 218 | CALayer *groupLayer = [[[_pieView layer] sublayers] objectAtIndex:0]; 219 | [groupLayer setOpacity:[(UISwitch *) sender isOn] ? 1.0 : 0.0]; 220 | } 221 | 222 | - (IBAction)toggleSliceLayers:(id)sender 223 | { 224 | CALayer *groupLayer = [[[_pieView layer] sublayers] objectAtIndex:1]; 225 | [groupLayer setOpacity:[(UISwitch *) sender isOn] ? 1.0 : 0.0]; 226 | } 227 | 228 | - (IBAction)toggleLabelLayers:(id)sender 229 | { 230 | CALayer *groupLayer = [[[_pieView layer] sublayers] objectAtIndex:2]; 231 | [groupLayer setOpacity:[(UISwitch *) sender isOn] ? 1.0 : 0.0]; 232 | } 233 | 234 | #pragma mark - Selection Mode 235 | 236 | - (IBAction)toggleSelectionMode:(id)sender 237 | { 238 | [_pieView setHighlightSelection:![_pieView highlightSelection]]; 239 | } 240 | 241 | #pragma mark - Perspective Methods (Hacks) 242 | 243 | - (void)updateSublayerTransform:(CATransform3D)transform zPosition:(int)zPosition 244 | { 245 | CALayer *pieLayer = [_pieView layer]; 246 | 247 | CALayer *lineLayerGroup = [[pieLayer sublayers] objectAtIndex:0]; 248 | [lineLayerGroup setSublayerTransform:transform]; 249 | 250 | CALayer *sliceLayerGroup = [[pieLayer sublayers] objectAtIndex:1]; 251 | [sliceLayerGroup setSublayerTransform:transform]; 252 | [[sliceLayerGroup sublayers] enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { 253 | [obj setZPosition:zPosition]; 254 | }]; 255 | 256 | CALayer *labelLayerGroup = [[pieLayer sublayers] objectAtIndex:2]; 257 | [labelLayerGroup setSublayerTransform:transform]; 258 | [[labelLayerGroup sublayers] enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { 259 | [obj setZPosition:zPosition * 2]; 260 | 261 | UIColor *color = zPosition == 0 ? [UIColor clearColor] : [[UIColor blackColor] colorWithAlphaComponent:0.5f]; 262 | [(CALayer *) obj setBackgroundColor:[color CGColor]]; 263 | }]; 264 | } 265 | 266 | - (IBAction)togglePerspective:(id)sender 267 | { 268 | CALayer *pieLayer = [_pieView layer]; 269 | if (CATransform3DIsIdentity([[[pieLayer sublayers] objectAtIndex:1] sublayerTransform])) { 270 | CATransform3D transform = CATransform3DIdentity; 271 | transform.m34 = 1.0f / -2500.0f; 272 | transform = CATransform3DRotate(transform, (CGFloat) M_PI_4, 0.0, 1.0, 0.0); 273 | [self updateSublayerTransform:transform zPosition:200]; 274 | } else { 275 | [self updateSublayerTransform:CATransform3DIdentity zPosition:0]; 276 | } 277 | } 278 | 279 | @end 280 | 281 | // Wraps a data value and color to make it easier to implement the data source + delegate callbacks. 282 | @implementation BTSSliceData 283 | 284 | + (id)sliceDataWithValue:(int)value color:(UIColor *)color 285 | { 286 | BTSSliceData *data = [[BTSSliceData alloc] init]; 287 | [data setValue:value]; 288 | [data setColor:color]; 289 | return data; 290 | } 291 | 292 | @end 293 | -------------------------------------------------------------------------------- /BTSPieChart/BTSPieChart-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | ${PRODUCT_NAME} 9 | CFBundleExecutable 10 | ${EXECUTABLE_NAME} 11 | CFBundleIconFiles 12 | 13 | CFBundleIdentifier 14 | $(PRODUCT_BUNDLE_IDENTIFIER) 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleName 18 | ${PRODUCT_NAME} 19 | CFBundlePackageType 20 | APPL 21 | CFBundleShortVersionString 22 | 1.0 23 | CFBundleSignature 24 | ???? 25 | CFBundleVersion 26 | 1.0 27 | LSRequiresIPhoneOS 28 | 29 | UILaunchStoryboardName 30 | MainStoryboard 31 | UIMainStoryboardFile 32 | MainStoryboard 33 | UIRequiredDeviceCapabilities 34 | 35 | armv7 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationLandscapeRight 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationPortrait 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /BTSPieChart/BTSPieChart-Prefix.pch: -------------------------------------------------------------------------------- 1 | // 2 | // Prefix header for all source files of the 'BTSPieChart' target in the 'BTSPieChart' project 3 | // 4 | 5 | #ifdef __OBJC__ 6 | 7 | #import 8 | #import 9 | 10 | #endif 11 | -------------------------------------------------------------------------------- /BTSPieChart/BTSPieLayer.h: -------------------------------------------------------------------------------- 1 | // 2 | // BTSPieLayer.h 3 | // 4 | // Copyright (c) 2011 Brian Coyner. All rights reserved. 5 | 6 | #import 7 | 8 | #import 9 | 10 | // Private implementation class. 11 | // 12 | // This is the root layer for the BTSPieView. 13 | 14 | @class BTSPieLayer; 15 | 16 | @interface BTSPieLayer : CALayer 17 | 18 | - (CALayer *)lineLayers; 19 | 20 | - (CALayer *)sliceLayers; 21 | 22 | - (CALayer *)labelLayers; 23 | 24 | - (void)removeAllPieLayers; 25 | 26 | @end 27 | -------------------------------------------------------------------------------- /BTSPieChart/BTSPieLayer.m: -------------------------------------------------------------------------------- 1 | // 2 | // BTSPieLayer.m 3 | // 4 | // Copyright (c) 2011 Brian Coyner. All rights reserved. 5 | 6 | #import "BTSPieLayer.h" 7 | 8 | typedef enum { 9 | BTSPieLayerLines, 10 | BTSPieLayerSlices, 11 | BTSPieLayerLabels 12 | } BTSPieLayerGroup; 13 | 14 | @implementation BTSPieLayer 15 | 16 | - (id)init 17 | { 18 | self = [super init]; 19 | if (self) { 20 | [self setContentsScale:[[UIScreen mainScreen] scale]]; 21 | [self addSublayer:[CALayer layer]]; // BTSPieLayerLines 22 | [self addSublayer:[CALayer layer]]; // BTSPieLayerSlices 23 | [self addSublayer:[CALayer layer]]; // BTSPieLayerLabels 24 | 25 | [[self sublayers] enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { 26 | [obj setContentsScale:[[UIScreen mainScreen] scale]]; 27 | }]; 28 | } 29 | return self; 30 | } 31 | 32 | - (CALayer *)lineLayers 33 | { 34 | return [[self sublayers] objectAtIndex:BTSPieLayerLines]; 35 | } 36 | 37 | - (CALayer *)sliceLayers 38 | { 39 | return [[self sublayers] objectAtIndex:BTSPieLayerSlices]; 40 | } 41 | 42 | - (CALayer *)labelLayers 43 | { 44 | return [[self sublayers] objectAtIndex:BTSPieLayerLabels]; 45 | } 46 | 47 | - (void)removeAllPieLayers 48 | { 49 | { 50 | NSArray *layers = [NSArray arrayWithArray:[[self lineLayers] sublayers]]; 51 | [layers enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { 52 | [obj removeFromSuperlayer]; 53 | }]; 54 | } 55 | 56 | { 57 | NSArray *layers = [NSArray arrayWithArray:[[self sliceLayers] sublayers]]; 58 | [layers enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { 59 | [obj removeFromSuperlayer]; 60 | }]; 61 | } 62 | 63 | { 64 | NSArray *layers = [NSArray arrayWithArray:[[self labelLayers] sublayers]]; 65 | [layers enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { 66 | [obj removeFromSuperlayer]; 67 | }]; 68 | } 69 | } 70 | 71 | @end 72 | -------------------------------------------------------------------------------- /BTSPieChart/BTSPieView.h: -------------------------------------------------------------------------------- 1 | // 2 | // BTSPieView.h 3 | // 4 | // Copyright (c) 2011 Brian Coyner. All rights reserved. 5 | // 6 | 7 | #import 8 | 9 | @protocol BTSPieViewDataSource; 10 | @protocol BTSPieViewDelegate; 11 | 12 | @interface BTSPieView : UIView 13 | 14 | @property (nonatomic, weak, readwrite) id dataSource; 15 | @property (nonatomic, weak, readwrite) id delegate; 16 | 17 | @property (nonatomic, assign, readwrite) CGFloat animationDuration; 18 | 19 | // simple hack to change selection behavior 20 | @property (nonatomic, assign, readwrite) BOOL highlightSelection; 21 | 22 | - (void)insertSliceAtIndex:(NSUInteger)index animate:(BOOL)animate; 23 | 24 | - (void)removeSliceAtIndex:(NSUInteger)index animate:(BOOL)animate; 25 | 26 | - (void)reloadSliceAtIndex:(NSUInteger)index animate:(BOOL)animate; 27 | 28 | - (void)reloadData; 29 | 30 | @end 31 | 32 | @protocol BTSPieViewDataSource 33 | 34 | - (NSUInteger)numberOfSlicesInPieView:(BTSPieView *)pieView; 35 | 36 | - (CGFloat)pieView:(BTSPieView *)pieView valueForSliceAtIndex:(NSUInteger)index; 37 | 38 | @end 39 | 40 | @protocol BTSPieViewDelegate 41 | 42 | - (void)pieView:(BTSPieView *)pieView willSelectSliceAtIndex:(NSInteger)index; 43 | 44 | - (void)pieView:(BTSPieView *)pieView didSelectSliceAtIndex:(NSInteger)index; 45 | 46 | - (void)pieView:(BTSPieView *)pieView willDeselectSliceAtIndex:(NSInteger)index; 47 | 48 | - (void)pieView:(BTSPieView *)pieView didDeselectSliceAtIndex:(NSInteger)index; 49 | 50 | - (UIColor *)pieView:(BTSPieView *)pieView colorForSliceAtIndex:(NSUInteger)index sliceCount:(NSUInteger)sliceCount; 51 | 52 | @end 53 | -------------------------------------------------------------------------------- /BTSPieChart/BTSPieView.mm: -------------------------------------------------------------------------------- 1 | // 2 | // BTSPieView.m 3 | // 4 | // Copyright (c) 2011 Brian Coyner. All rights reserved. 5 | // 6 | 7 | #import "BTSPieView.h" 8 | 9 | #import "BTSPieViewValues.h" 10 | #import "BTSPieLayer.h" 11 | #import "BTSSliceLayer.h" 12 | 13 | static float const kBTSPieViewSelectionOffset = 20.0f; 14 | 15 | // Used as a CAAnimationDelegate when animating existing slices 16 | @interface BTSSliceLayerExistingLayerDelegate : NSObject 17 | 18 | @property (nonatomic, weak, readwrite) id animationDelegate; 19 | 20 | @end 21 | 22 | @interface BTSSliceLayerAddAtBeginningLayerDelegate : NSObject 23 | 24 | @property (nonatomic, weak, readwrite) id animationDelegate; 25 | 26 | @end 27 | 28 | @interface BTSSliceLayerAddInMiddleLayerDelegate : NSObject 29 | 30 | @property (nonatomic, weak, readwrite) id animationDelegate; 31 | @property (nonatomic, assign, readwrite) CGFloat initialSliceAngle; 32 | 33 | @end 34 | 35 | @interface BTSPieView () { 36 | 37 | NSInteger _selectedSliceIndex; 38 | 39 | CADisplayLink *_displayLink; 40 | 41 | NSMutableArray *_animations; 42 | NSMutableArray *_layersToRemove; 43 | NSMutableArray *_deletionStack; 44 | 45 | BTSSliceLayerExistingLayerDelegate *_existingLayerDelegate; 46 | BTSSliceLayerAddAtBeginningLayerDelegate *_addAtBeginningLayerDelegate; 47 | BTSSliceLayerAddInMiddleLayerDelegate *_addInMiddleLayerDelegate; 48 | 49 | NSNumberFormatter *_labelFormatter; 50 | 51 | CGPoint _center; 52 | CGFloat _radius; 53 | } 54 | 55 | // C-helper functions 56 | CGPathRef CGPathCreateArc(CGPoint center, CGFloat radius, CGFloat startAngle, CGFloat endAngle); 57 | 58 | CGPathRef CGPathCreateArcLineForAngle(CGPoint center, CGFloat radius, CGFloat angle); 59 | 60 | void BTSUpdateLabelPosition(CALayer *labelLayer, CGPoint center, CGFloat radius, CGFloat startAngle, CGFloat endAngle); 61 | 62 | void BTSUpdateAllLayers(BTSPieLayer *pieLayer, NSUInteger layerIndex, CGPoint center, CGFloat radius, CGFloat startAngle, CGFloat endAngle); 63 | 64 | void BTSUpdateLayers(NSArray *sliceLayers, NSArray *labelLayers, NSArray *lineLayers, NSUInteger layerIndex, CGPoint center, CGFloat radius, CGFloat startAngle, CGFloat endAngle); 65 | 66 | CGFloat BTSLookupPreviousLayerAngle(NSArray *pieLayers, NSUInteger currentPieLayerIndex, CGFloat defaultAngle); 67 | 68 | @end 69 | 70 | @implementation BTSPieView 71 | 72 | @synthesize dataSource = _dataSource; 73 | @synthesize delegate = _delegate; 74 | @synthesize animationDuration = _animationDuration; 75 | @synthesize highlightSelection = _highlightSelection; 76 | 77 | #pragma mark - Custom Layer Initialization 78 | 79 | + (Class)layerClass 80 | { 81 | return [BTSPieLayer class]; 82 | } 83 | 84 | #pragma mark - View Initialization 85 | 86 | - (void)initView 87 | { 88 | _animationDuration = 0.2f; 89 | _highlightSelection = YES; 90 | 91 | _labelFormatter = [[NSNumberFormatter alloc] init]; 92 | [_labelFormatter setNumberStyle:NSNumberFormatterPercentStyle]; 93 | 94 | _selectedSliceIndex = -1; 95 | _animations = [[NSMutableArray alloc] init]; 96 | 97 | _layersToRemove = [[NSMutableArray alloc] init]; 98 | _deletionStack = [[NSMutableArray alloc] init]; 99 | 100 | _existingLayerDelegate = [[BTSSliceLayerExistingLayerDelegate alloc] init]; 101 | [_existingLayerDelegate setAnimationDelegate:self]; 102 | 103 | _addAtBeginningLayerDelegate = [[BTSSliceLayerAddAtBeginningLayerDelegate alloc] init]; 104 | [_addAtBeginningLayerDelegate setAnimationDelegate:self]; 105 | 106 | _addInMiddleLayerDelegate = [[BTSSliceLayerAddInMiddleLayerDelegate alloc] init]; 107 | [_addInMiddleLayerDelegate setAnimationDelegate:self]; 108 | 109 | _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(updateTimerFired:)]; 110 | [_displayLink setPaused:YES]; 111 | [_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode]; 112 | } 113 | 114 | - (id)initWithCoder:(NSCoder *)aDecoder 115 | { 116 | self = [super initWithCoder:aDecoder]; 117 | if (self) { 118 | [self initView]; 119 | } 120 | 121 | return self; 122 | } 123 | 124 | - (id)init 125 | { 126 | self = [super init]; 127 | if (self) { 128 | [self initView]; 129 | } 130 | 131 | return self; 132 | } 133 | 134 | #pragma mark - View Clean Up 135 | 136 | - (void)dealloc 137 | { 138 | [_displayLink invalidate]; 139 | _displayLink = nil; 140 | } 141 | 142 | #pragma mark - Layout Hack 143 | 144 | - (void)layoutSubviews 145 | { 146 | // Calculate the center and radius based on the parent layer's bounds. This version 147 | // of the BTSPieChart assumes the view does not change size. 148 | CGRect parentLayerBounds = [[self layer] bounds]; 149 | CGFloat centerX = parentLayerBounds.size.width / 2.0f; 150 | CGFloat centerY = parentLayerBounds.size.height / 2.0f; 151 | _center = CGPointMake(centerX, centerY); 152 | 153 | // Reduce the radius just a bit so the the pie chart layers do not hug the edge of the view. 154 | _radius = MIN(centerX, centerY) - 10; 155 | 156 | [self refreshLayers]; 157 | } 158 | 159 | - (void)beginCATransaction 160 | { 161 | [CATransaction begin]; 162 | [CATransaction setAnimationDuration:_animationDuration]; 163 | } 164 | 165 | #pragma mark - Reload Pie View(No Animation) 166 | 167 | - (BTSSliceLayer *)insertSliceLayerAtIndex:(NSUInteger)index color:(UIColor *)color 168 | { 169 | BTSSliceLayer *sliceLayer = [BTSSliceLayer layerWithColor:[color CGColor]]; 170 | 171 | BTSPieLayer *pieLayer = (BTSPieLayer *)[self layer]; 172 | [[pieLayer sliceLayers] insertSublayer:sliceLayer atIndex:(unsigned)index]; 173 | return sliceLayer; 174 | } 175 | 176 | - (CATextLayer *)insertLabelLayerAtIndex:(NSUInteger)index value:(double)value 177 | { 178 | CATextLayer *labelLayer = [BTSPieView createLabelLayer]; 179 | [labelLayer setString:[_labelFormatter stringFromNumber:@(value)]]; 180 | 181 | BTSPieLayer *pieLayer = (BTSPieLayer *)[self layer]; 182 | CALayer *layer = [pieLayer labelLayers]; 183 | [layer insertSublayer:labelLayer atIndex:(unsigned)index]; 184 | return labelLayer; 185 | } 186 | 187 | - (CAShapeLayer *)insertLineLayerAtIndex:(NSUInteger)index color:(UIColor *)color 188 | { 189 | CAShapeLayer *lineLayer = [CAShapeLayer layer]; 190 | [lineLayer setStrokeColor:[color CGColor]]; 191 | 192 | BTSPieLayer *pieLayer = (BTSPieLayer *)[self layer]; 193 | [[pieLayer lineLayers] insertSublayer:lineLayer atIndex:(unsigned)index]; 194 | 195 | return lineLayer; 196 | } 197 | 198 | - (void)reloadData 199 | { 200 | [CATransaction begin]; 201 | [CATransaction setDisableActions:YES]; 202 | 203 | BTSPieLayer *parentLayer = (BTSPieLayer *)[self layer]; 204 | [parentLayer removeAllPieLayers]; 205 | 206 | if (_dataSource) { 207 | 208 | NSUInteger sliceCount = [_dataSource numberOfSlicesInPieView:self]; 209 | 210 | BTSPieViewValues values((unsigned int)sliceCount, ^(NSUInteger index) { 211 | return [self->_dataSource pieView:self valueForSliceAtIndex:(unsigned)index]; 212 | }); 213 | 214 | CGFloat startAngle = (CGFloat)-M_PI_2; 215 | CGFloat endAngle = startAngle; 216 | 217 | for (NSUInteger sliceIndex = 0; sliceIndex < sliceCount; sliceIndex++) { 218 | 219 | endAngle += values.angles()[sliceIndex]; 220 | 221 | UIColor *color = [_delegate pieView:self colorForSliceAtIndex:sliceIndex sliceCount:sliceCount]; 222 | [self insertSliceLayerAtIndex:sliceIndex color:color]; 223 | [self insertLabelLayerAtIndex:sliceIndex value:values.percentages()[sliceIndex]]; 224 | [self insertLineLayerAtIndex:sliceIndex color:color]; 225 | 226 | BTSUpdateAllLayers(parentLayer, sliceIndex, _center, _radius, startAngle, endAngle); 227 | 228 | startAngle = endAngle; 229 | } 230 | } 231 | [CATransaction setDisableActions:NO]; 232 | [CATransaction commit]; 233 | } 234 | 235 | #pragma mark - Insert Slice 236 | 237 | - (void)insertSliceAtIndex:(NSUInteger)indexToInsert animate:(BOOL)animate 238 | { 239 | if (!animate) { 240 | [self reloadData]; 241 | return; 242 | } 243 | 244 | if (_dataSource) { 245 | 246 | [self beginCATransaction]; 247 | 248 | NSUInteger sliceCount = [_dataSource numberOfSlicesInPieView:self]; 249 | BTSPieViewValues values((unsigned int)sliceCount, ^(NSUInteger sliceIndex) { 250 | return [self->_dataSource pieView:self valueForSliceAtIndex:sliceIndex]; 251 | }); 252 | 253 | CGFloat startAngle = (CGFloat)-M_PI_2; 254 | CGFloat endAngle = startAngle; 255 | 256 | for (NSUInteger currentIndex = 0; currentIndex < sliceCount; currentIndex++) { 257 | 258 | // Make no implicit transactions are creating (e.g. when adding the new slice we don't want a "fade in" effect) 259 | [CATransaction setDisableActions:YES]; 260 | 261 | endAngle += values.angles()[currentIndex]; 262 | 263 | BTSSliceLayer *sliceLayer; 264 | if (indexToInsert == currentIndex) { 265 | sliceLayer = [self insertSliceAtIndex:currentIndex values:&values startAngle:startAngle endAngle:endAngle]; 266 | } else { 267 | sliceLayer = [self updateSliceAtIndex:currentIndex values:&values]; 268 | } 269 | 270 | [CATransaction setDisableActions:NO]; 271 | 272 | // Remember because "sliceAngle" is a dynamic property this ends up calling the actionForLayer:forKey: method on each layer with a non-nil delegate 273 | [sliceLayer setSliceAngle:endAngle]; 274 | [sliceLayer setDelegate:nil]; 275 | 276 | startAngle = endAngle; 277 | } 278 | 279 | [CATransaction commit]; 280 | } 281 | } 282 | 283 | - (BTSSliceLayer *)insertSliceAtIndex:(NSUInteger)index values:(BTSPieViewValues *)values startAngle:(CGFloat)startAngle endAngle:(CGFloat)endAngle 284 | { 285 | NSUInteger sliceCount = values->count(); 286 | UIColor *color = [_delegate pieView:self colorForSliceAtIndex:index sliceCount:sliceCount]; 287 | 288 | BTSSliceLayer *sliceLayer = [self insertSliceLayerAtIndex:index color:color]; 289 | id delegate = [self delegateForSliceAtIndex:index sliceCount:sliceCount]; 290 | [sliceLayer setDelegate:delegate]; 291 | 292 | CGFloat initialLabelAngle = [self initialLabelAngleForSliceAtIndex:index sliceCount:sliceCount startAngle:startAngle endAngle:endAngle]; 293 | CATextLayer *labelLayer = [self insertLabelLayerAtIndex:index value:values->percentages()[index]]; 294 | BTSUpdateLabelPosition(labelLayer, _center, _radius, initialLabelAngle, initialLabelAngle); 295 | 296 | // Special Case... 297 | // If the delegate is the "add in middle", then the "initial label angle" is also the delegate's starting angle. 298 | if (delegate == _addInMiddleLayerDelegate) { 299 | [_addInMiddleLayerDelegate setInitialSliceAngle:initialLabelAngle]; 300 | } 301 | 302 | [self insertLineLayerAtIndex:index color:color]; 303 | 304 | return sliceLayer; 305 | } 306 | 307 | - (BTSSliceLayer *)updateSliceAtIndex:(NSUInteger)currentIndex values:(BTSPieViewValues *)values 308 | { 309 | BTSPieLayer *pieLayer = (BTSPieLayer *)[self layer]; 310 | 311 | NSArray *sliceLayers = [[pieLayer sliceLayers] sublayers]; 312 | BTSSliceLayer *sliceLayer = (BTSSliceLayer *)sliceLayers[currentIndex]; 313 | [sliceLayer setDelegate:_existingLayerDelegate]; 314 | 315 | NSArray *labelLayers = [[pieLayer labelLayers] sublayers]; 316 | CATextLayer *labelLayer = labelLayers[currentIndex]; 317 | double value = values->percentages()[currentIndex]; 318 | NSString *label = [_labelFormatter stringFromNumber:@(value)]; 319 | [labelLayer setString:label]; 320 | return sliceLayer; 321 | } 322 | 323 | - (id)delegateForSliceAtIndex:(NSUInteger)currentIndex sliceCount:(NSUInteger)sliceCount 324 | { 325 | // The inserted layer animates differently depending on where the new layer is inserted. 326 | id delegate; 327 | if (currentIndex == 0) { 328 | delegate = _addAtBeginningLayerDelegate; 329 | } else if (currentIndex + 1 == sliceCount) { 330 | delegate = nil; 331 | } else { 332 | delegate = _addInMiddleLayerDelegate; 333 | } 334 | return delegate; 335 | } 336 | 337 | - (CGFloat)initialLabelAngleForSliceAtIndex:(NSUInteger)currentIndex sliceCount:(NSUInteger)sliceCount startAngle:(CGFloat)startAngle endAngle:(CGFloat)endAngle 338 | { 339 | // The inserted layer animates differently depending on where the new layer is inserted. 340 | CGFloat initialLabelAngle; 341 | 342 | if (currentIndex == 0) { 343 | initialLabelAngle = startAngle; 344 | } else if (currentIndex + 1 == sliceCount) { 345 | initialLabelAngle = endAngle; 346 | } else { 347 | BTSPieLayer *pieLayer = (BTSPieLayer *)[self layer]; 348 | NSArray *pieLayers = [[pieLayer sliceLayers] sublayers]; 349 | initialLabelAngle = BTSLookupPreviousLayerAngle(pieLayers, currentIndex, (CGFloat)-M_PI_2); 350 | } 351 | return initialLabelAngle; 352 | } 353 | 354 | #pragma mark - Remove Slice 355 | 356 | - (void)removeSliceAtIndex:(NSUInteger)indexToRemove animate:(BOOL)animate 357 | { 358 | if (!animate) { 359 | [self reloadData]; 360 | return; 361 | } 362 | 363 | if (_delegate) { 364 | 365 | BTSPieLayer *parentLayer = (BTSPieLayer *)[self layer]; 366 | NSArray *sliceLayers = [[parentLayer sliceLayers] sublayers]; 367 | NSArray *labelLayers = [[parentLayer labelLayers] sublayers]; 368 | NSArray *lineLayers = [[parentLayer lineLayers] sublayers]; 369 | 370 | CAShapeLayer *sliceLayerToRemove = sliceLayers[indexToRemove]; 371 | CATextLayer *labelLayerToRemove = labelLayers[indexToRemove]; 372 | CALayer *lineLayerToRemove = lineLayers[indexToRemove]; 373 | 374 | [_layersToRemove addObjectsFromArray:@[lineLayerToRemove, sliceLayerToRemove, labelLayerToRemove]]; 375 | 376 | [self beginCATransaction]; 377 | 378 | NSUInteger current = [_layersToRemove count]; 379 | [CATransaction setCompletionBlock:^{ 380 | if (current == [self->_layersToRemove count]) { 381 | [self->_layersToRemove enumerateObjectsUsingBlock:^(id obj, NSUInteger index, BOOL *stop) { 382 | [obj removeFromSuperlayer]; 383 | }]; 384 | 385 | [self->_layersToRemove removeAllObjects]; 386 | } 387 | }]; 388 | 389 | NSUInteger sliceCount = [_dataSource numberOfSlicesInPieView:self]; 390 | 391 | if (sliceCount > 0) { 392 | 393 | [CATransaction setDisableActions:YES]; 394 | [labelLayerToRemove setHidden:YES]; 395 | [CATransaction setDisableActions:NO]; 396 | 397 | BTSPieViewValues values((unsigned int)sliceCount, ^(NSUInteger index) { 398 | return [self->_dataSource pieView:self valueForSliceAtIndex:index]; 399 | }); 400 | 401 | CGFloat startAngle = (CGFloat)-M_PI_2; 402 | CGFloat endAngle = startAngle; 403 | for (NSUInteger sliceIndex = 0; sliceIndex < [sliceLayers count]; sliceIndex++) { 404 | 405 | BTSSliceLayer *sliceLayer = (BTSSliceLayer *)sliceLayers[sliceIndex]; 406 | [sliceLayer setDelegate:_existingLayerDelegate]; 407 | 408 | NSUInteger modelIndex = sliceIndex <= indexToRemove ? sliceIndex : sliceIndex - 1; 409 | 410 | CGFloat currentEndAngle; 411 | if (sliceIndex == indexToRemove) { 412 | currentEndAngle = endAngle; 413 | } else { 414 | double value = values.percentages()[modelIndex]; 415 | NSString *label = [_labelFormatter stringFromNumber:@(value)]; 416 | CATextLayer *labelLayer = labelLayers[sliceIndex]; 417 | [labelLayer setString:label]; 418 | 419 | endAngle += values.angles()[modelIndex]; 420 | currentEndAngle = endAngle; 421 | } 422 | 423 | [sliceLayer setSliceAngle:currentEndAngle]; 424 | } 425 | } 426 | 427 | [CATransaction commit]; 428 | 429 | [self maybeNotifyDelegateOfSelectionChangeFrom:_selectedSliceIndex to:-1]; 430 | } 431 | } 432 | 433 | #pragma mark - Reload Slice Value 434 | 435 | - (void)reloadSliceAtIndex:(NSUInteger)index animate:(BOOL)animate 436 | { 437 | if (!animate) { 438 | [self reloadData]; 439 | return; 440 | } 441 | 442 | if (_dataSource) { 443 | 444 | [self beginCATransaction]; 445 | 446 | BTSPieLayer *parentLayer = (BTSPieLayer *)[self layer]; 447 | NSArray *sliceLayers = [[parentLayer sliceLayers] sublayers]; 448 | NSArray *labelLayers = [[parentLayer labelLayers] sublayers]; 449 | 450 | NSUInteger sliceCount = [_dataSource numberOfSlicesInPieView:self]; 451 | 452 | BTSPieViewValues values((unsigned int)sliceCount, ^(NSUInteger sliceIndex) { 453 | return [self->_dataSource pieView:self valueForSliceAtIndex:sliceIndex]; 454 | }); 455 | 456 | // For simplicity, the start angle is always zero... no reason it can't be any valid angle in radians. 457 | CGFloat endAngle = (CGFloat)-M_PI_2; 458 | 459 | // We are updating existing layer values (viz. not adding, or removing). We simply iterate each slice layer and 460 | // adjust the start and end angles. 461 | for (NSUInteger sliceIndex = 0; sliceIndex < sliceCount; sliceIndex++) { 462 | 463 | BTSSliceLayer *sliceLayer = (BTSSliceLayer *)sliceLayers[sliceIndex]; 464 | [sliceLayer setDelegate:_existingLayerDelegate]; 465 | 466 | endAngle += values.angles()[sliceIndex]; 467 | [sliceLayer setSliceAngle:endAngle]; 468 | 469 | CATextLayer *labelLayer = (CATextLayer *)labelLayers[sliceIndex]; 470 | double value = values.percentages()[sliceIndex]; 471 | NSNumber *valueAsNumber = @(value); 472 | NSString *label = [_labelFormatter stringFromNumber:valueAsNumber]; 473 | [labelLayer setString:label]; 474 | } 475 | 476 | [CATransaction commit]; 477 | } 478 | } 479 | 480 | - (void)refreshLayers 481 | { 482 | BTSPieLayer *pieLayer = (BTSPieLayer *)[self layer]; 483 | NSArray *sliceLayers = [[pieLayer sliceLayers] sublayers]; 484 | NSArray *labelLayers = [[pieLayer labelLayers] sublayers]; 485 | NSArray *lineLayers = [[pieLayer lineLayers] sublayers]; 486 | 487 | [sliceLayers enumerateObjectsUsingBlock:^(id obj, NSUInteger index, BOOL *stop) { 488 | CGFloat startAngle = BTSLookupPreviousLayerAngle(sliceLayers, index, (CGFloat)-M_PI_2); 489 | CGFloat endAngle = (CGFloat)[[obj valueForKey:kBTSSliceLayerAngle] doubleValue]; 490 | BTSUpdateLayers(sliceLayers, labelLayers, lineLayers, index, self->_center, self->_radius, startAngle, endAngle); 491 | }]; 492 | } 493 | 494 | #pragma mark - Animation Delegate + CADisplayLink Callback 495 | 496 | - (void)updateTimerFired:(CADisplayLink *)displayLink 497 | { 498 | BTSPieLayer *parentLayer = (BTSPieLayer *)[self layer]; 499 | NSArray *pieLayers = [[parentLayer sliceLayers] sublayers]; 500 | NSArray *labelLayers = [[parentLayer labelLayers] sublayers]; 501 | NSArray *lineLayers = [[parentLayer lineLayers] sublayers]; 502 | 503 | CGPoint center = _center; 504 | CGFloat radius = _radius; 505 | 506 | [CATransaction setDisableActions:YES]; 507 | 508 | NSUInteger index = 0; 509 | for (BTSSliceLayer *currentPieLayer in pieLayers) { 510 | CGFloat interpolatedStartAngle = BTSLookupPreviousLayerAngle(pieLayers, index, (CGFloat)-M_PI_2); 511 | BTSSliceLayer *presentationLayer = (BTSSliceLayer *)[currentPieLayer presentationLayer]; 512 | CGFloat interpolatedEndAngle = [presentationLayer sliceAngle]; 513 | 514 | BTSUpdateLayers(pieLayers, labelLayers, lineLayers, index, center, radius, interpolatedStartAngle, interpolatedEndAngle); 515 | ++index; 516 | } 517 | [CATransaction setDisableActions:NO]; 518 | } 519 | 520 | - (void)animationDidStart:(CAAnimation *)animation 521 | { 522 | [_displayLink setPaused:NO]; 523 | [_animations addObject:animation]; 524 | } 525 | 526 | - (void)animationDidStop:(CAAnimation *)animation finished:(BOOL)animationCompleted 527 | { 528 | [_animations removeObject:animation]; 529 | 530 | if ([_animations count] == 0) { 531 | [_displayLink setPaused:YES]; 532 | } 533 | } 534 | 535 | #pragma mark - Touch Handing (Selection Notification) 536 | 537 | - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event 538 | { 539 | [self touchesMoved:touches withEvent:event]; 540 | } 541 | 542 | - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event 543 | { 544 | UITouch *touch = [touches anyObject]; 545 | CGPoint point = [touch locationInView:self]; 546 | 547 | __block NSInteger selectedIndex = -1; 548 | 549 | BTSPieLayer *pieLayer = (BTSPieLayer *)[self layer]; 550 | NSArray *lineLayers = [[pieLayer lineLayers] sublayers]; 551 | NSArray *sliceLayers = [[pieLayer sliceLayers] sublayers]; 552 | NSArray *labelLayers = [[pieLayer labelLayers] sublayers]; 553 | 554 | [sliceLayers enumerateObjectsUsingBlock:^(id obj, NSUInteger index, BOOL *stop) { 555 | BTSSliceLayer *sliceLayer = (BTSSliceLayer *)obj; 556 | CGPathRef path = [sliceLayer path]; 557 | 558 | CGFloat startAngle = BTSLookupPreviousLayerAngle(sliceLayers, index, (CGFloat)-M_PI_2); 559 | 560 | // NOTE: in this demo code, the touch handling does not know about any applied transformations (i.e. perspective) 561 | if (CGPathContainsPoint(path, &CGAffineTransformIdentity, point, 0)) { 562 | 563 | if (self->_highlightSelection) { 564 | [sliceLayer setStrokeColor:[[UIColor whiteColor] CGColor]]; 565 | [sliceLayer setLineWidth:2.0]; 566 | [sliceLayer setZPosition:1]; 567 | } else { 568 | double endAngle = [sliceLayer sliceAngle]; 569 | 570 | CGFloat deltaAngle = (CGFloat)(((endAngle + startAngle) / 2.0)); 571 | 572 | CGFloat x = (CGFloat)(kBTSPieViewSelectionOffset * cos(deltaAngle)); 573 | CGFloat y = (CGFloat)(kBTSPieViewSelectionOffset * sin(deltaAngle)); 574 | 575 | CGAffineTransform translationTransform = CGAffineTransformMakeTranslation(x, y); 576 | [sliceLayer setAffineTransform:translationTransform]; 577 | 578 | [labelLayers[index] setAffineTransform:translationTransform]; 579 | [lineLayers[index] setAffineTransform:translationTransform]; 580 | } 581 | 582 | selectedIndex = (NSInteger)index; 583 | } else { 584 | [sliceLayer setAffineTransform:CGAffineTransformIdentity]; 585 | [labelLayers[index] setAffineTransform:CGAffineTransformIdentity]; 586 | [lineLayers[index] setAffineTransform:CGAffineTransformIdentity]; 587 | [sliceLayer setLineWidth:0.0]; 588 | [sliceLayer setZPosition:0]; 589 | } 590 | }]; 591 | 592 | [self maybeNotifyDelegateOfSelectionChangeFrom:_selectedSliceIndex to:selectedIndex]; 593 | } 594 | 595 | 596 | #pragma mark - Selection Notification 597 | 598 | - (void)maybeNotifyDelegateOfSelectionChangeFrom:(NSInteger)previousSelection to:(NSInteger)newSelection 599 | { 600 | if (previousSelection != newSelection) { 601 | 602 | if (previousSelection != -1) { 603 | [_delegate pieView:self willDeselectSliceAtIndex:previousSelection]; 604 | } 605 | 606 | _selectedSliceIndex = newSelection; 607 | 608 | if (newSelection != -1) { 609 | [_delegate pieView:self willSelectSliceAtIndex:newSelection]; 610 | 611 | if (previousSelection != -1) { 612 | [_delegate pieView:self didDeselectSliceAtIndex:previousSelection]; 613 | } 614 | 615 | [_delegate pieView:self didSelectSliceAtIndex:newSelection]; 616 | } else { 617 | if (previousSelection != -1) { 618 | [_delegate pieView:self didDeselectSliceAtIndex:previousSelection]; 619 | } 620 | } 621 | } 622 | } 623 | 624 | #pragma mark - Pie Layer Creation Method 625 | 626 | + (CATextLayer *)createLabelLayer 627 | { 628 | CATextLayer *textLayer = [CATextLayer layer]; 629 | [textLayer setContentsScale:[[UIScreen mainScreen] scale]]; 630 | CGFontRef font = CGFontCreateWithFontName((__bridge CFStringRef)[[UIFont boldSystemFontOfSize:17.0] fontName]); 631 | [textLayer setFont:font]; 632 | CFRelease(font); 633 | [textLayer setFontSize:17.0]; 634 | [textLayer setAnchorPoint:CGPointMake(0.5, 0.5)]; 635 | [textLayer setAlignmentMode:kCAAlignmentCenter]; 636 | 637 | NSDictionary *attributes = @{ 638 | NSFontAttributeName: [UIFont systemFontOfSize:17.0] 639 | }; 640 | CGSize size = [@"100.00%" sizeWithAttributes:attributes]; 641 | [textLayer setBounds:CGRectMake(0.0, 0.0, size.width, size.height)]; 642 | return textLayer; 643 | } 644 | 645 | #pragma mark - Function Helpers 646 | 647 | // Helper method to create an arc path for a layer 648 | CGPathRef CGPathCreateArc(CGPoint center, CGFloat radius, CGFloat startAngle, CGFloat endAngle) { 649 | CGMutablePathRef path = CGPathCreateMutable(); 650 | 651 | CGPathMoveToPoint(path, NULL, center.x, center.y); 652 | CGPathAddArc(path, NULL, center.x, center.y, radius, startAngle, endAngle, 0); 653 | CGPathCloseSubpath(path); 654 | return path; 655 | } 656 | 657 | CGPathRef CGPathCreateArcLineForAngle(CGPoint center, CGFloat radius, CGFloat angle) { 658 | CGMutablePathRef linePath = CGPathCreateMutable(); 659 | CGPathMoveToPoint(linePath, NULL, center.x, center.y); 660 | CGPathAddLineToPoint(linePath, NULL, (CGFloat)(center.x + (radius) * cos(angle)), (CGFloat)(center.y + (radius) * sin(angle))); 661 | return linePath; 662 | } 663 | 664 | void BTSUpdateLabelPosition(CALayer *labelLayer, CGPoint center, CGFloat radius, CGFloat startAngle, CGFloat endAngle) { 665 | CGFloat midAngle = (startAngle + endAngle) / 2.0f; 666 | CGFloat halfRadius = radius / 2.0f; 667 | [labelLayer setPosition:CGPointMake((CGFloat)(center.x + (halfRadius * cos(midAngle))), (CGFloat)(center.y + (halfRadius * sin(midAngle))))]; 668 | } 669 | 670 | void BTSUpdateLayers(NSArray *sliceLayers, NSArray *labelLayers, NSArray *lineLayers, NSUInteger layerIndex, CGPoint center, CGFloat radius, CGFloat startAngle, CGFloat endAngle) { 671 | 672 | { 673 | CAShapeLayer *lineLayer = lineLayers[layerIndex]; 674 | 675 | CGPathRef linePath = CGPathCreateArcLineForAngle(center, radius, endAngle); 676 | [lineLayer setPath:linePath]; 677 | CFRelease(linePath); 678 | } 679 | 680 | { 681 | CAShapeLayer *sliceLayer = sliceLayers[layerIndex]; 682 | 683 | CGPathRef path = CGPathCreateArc(center, radius, startAngle, endAngle); 684 | [sliceLayer setPath:path]; 685 | CFRelease(path); 686 | } 687 | 688 | { 689 | CALayer *labelLayer = labelLayers[layerIndex]; 690 | BTSUpdateLabelPosition(labelLayer, center, radius, startAngle, endAngle); 691 | } 692 | } 693 | 694 | void BTSUpdateAllLayers(BTSPieLayer *pieLayer, NSUInteger layerIndex, CGPoint center, CGFloat radius, CGFloat startAngle, CGFloat endAngle) { 695 | BTSUpdateLayers([[pieLayer sliceLayers] sublayers], [[pieLayer labelLayers] sublayers], [[pieLayer lineLayers] sublayers], layerIndex, center, radius, startAngle, endAngle); 696 | } 697 | 698 | CGFloat BTSLookupPreviousLayerAngle(NSArray *pieLayers, NSUInteger currentPieLayerIndex, CGFloat defaultAngle) { 699 | BTSSliceLayer *sliceLayer; 700 | if (currentPieLayerIndex == 0) { 701 | sliceLayer = nil; 702 | } else { 703 | sliceLayer = pieLayers[currentPieLayerIndex - 1]; 704 | } 705 | 706 | return (sliceLayer == nil) ? defaultAngle : [[sliceLayer presentationLayer] sliceAngle]; 707 | } 708 | 709 | @end 710 | 711 | #pragma mark - Existing Layer Animation Delegate 712 | 713 | @implementation BTSSliceLayerExistingLayerDelegate 714 | 715 | @synthesize animationDelegate = _animationDelegate; 716 | 717 | - (id)actionForLayer:(CALayer *)layer forKey:(NSString *)event 718 | { 719 | if ([kBTSSliceLayerAngle isEqual:event]) { 720 | 721 | CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:event]; 722 | NSNumber *currentAngle = [[layer presentationLayer] valueForKey:event]; 723 | [animation setFromValue:currentAngle]; 724 | [animation setDelegate:_animationDelegate]; 725 | [animation setTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionDefault]]; 726 | 727 | return animation; 728 | } else { 729 | return nil; 730 | } 731 | } 732 | 733 | @end 734 | 735 | #pragma mark - New Layer Animation Delegate 736 | 737 | @implementation BTSSliceLayerAddAtBeginningLayerDelegate 738 | 739 | @synthesize animationDelegate = _animationDelegate; 740 | 741 | - (id)actionForLayer:(CALayer *)layer forKey:(NSString *)event 742 | { 743 | if ([kBTSSliceLayerAngle isEqualToString:event]) { 744 | CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:kBTSSliceLayerAngle]; 745 | 746 | [animation setFromValue:@(-M_PI_2)]; 747 | [animation setDelegate:_animationDelegate]; 748 | [animation setTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionDefault]]; 749 | 750 | return animation; 751 | } else { 752 | return nil; 753 | } 754 | } 755 | 756 | @end 757 | 758 | #pragma mark - Add Layer In Middle Animation Delegate 759 | 760 | @implementation BTSSliceLayerAddInMiddleLayerDelegate 761 | 762 | @synthesize animationDelegate = _animationDelegate; 763 | @synthesize initialSliceAngle = _initialSliceAngle; 764 | 765 | - (id)actionForLayer:(CALayer *)layer forKey:(NSString *)event 766 | { 767 | if ([kBTSSliceLayerAngle isEqualToString:event]) { 768 | CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:kBTSSliceLayerAngle]; 769 | 770 | [animation setFromValue:@(_initialSliceAngle)]; 771 | [animation setDelegate:_animationDelegate]; 772 | [animation setTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionDefault]]; 773 | 774 | return animation; 775 | } else { 776 | return nil; 777 | } 778 | } 779 | 780 | @end 781 | 782 | -------------------------------------------------------------------------------- /BTSPieChart/BTSPieViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // BTSPieViewController.h 3 | // 4 | // Copyright (c) 2011 Brian Coyner. All rights reserved. 5 | // 6 | 7 | #import 8 | 9 | @class BTSPieView; 10 | 11 | @interface BTSPieViewController : UIViewController 12 | 13 | - (BTSPieView *)pieView; 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /BTSPieChart/BTSPieViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // BTSPieViewController.m 3 | // 4 | // Copyright (c) 2011 Brian Coyner. All rights reserved. 5 | // 6 | 7 | #import "BTSPieViewController.h" 8 | 9 | @implementation BTSPieViewController 10 | 11 | - (BTSPieView *)pieView 12 | { 13 | return (BTSPieView *)[[[self view] subviews] lastObject]; 14 | } 15 | 16 | @end 17 | -------------------------------------------------------------------------------- /BTSPieChart/BTSPieViewValues.h: -------------------------------------------------------------------------------- 1 | // 2 | // BTSPieViewValues.h 3 | // 4 | // Copyright (c) 2011 Brian Coyner. All rights reserved. 5 | // 6 | 7 | // C++ class that collects and calculates values from the data source. 8 | 9 | class BTSPieViewValues { 10 | public: 11 | typedef CGFloat(^BTSFetchBlock)(NSUInteger index); 12 | 13 | BTSPieViewValues(unsigned int sliceCount, BTSFetchBlock fetchBlock) 14 | : _sliceCount (sliceCount) 15 | , _percentages(new double [sliceCount]) 16 | , _values(new double [sliceCount]) 17 | , _angles(new CGFLOAT_TYPE[sliceCount]) { 18 | double sum = 0.0; 19 | for (NSUInteger currentIndex = 0; currentIndex < sliceCount; currentIndex++) { 20 | _values[currentIndex] = fetchBlock(currentIndex); 21 | sum += _values[currentIndex]; 22 | } 23 | 24 | CGFloat twoPie = (CGFloat) M_PI * 2.0; 25 | for (int currentIndex = 0; currentIndex < sliceCount; currentIndex++) { 26 | double percentage = _values[currentIndex] / sum; 27 | _percentages[currentIndex] = percentage; 28 | 29 | CGFloat angle = (CGFloat) (twoPie * percentage); 30 | _angles[currentIndex] = angle; 31 | } 32 | } 33 | 34 | ~BTSPieViewValues() { 35 | delete[] _percentages; 36 | delete[] _values; 37 | delete[] _angles; 38 | } 39 | 40 | const double *values() const { 41 | return _values; 42 | } 43 | 44 | const double *percentages() const { 45 | return _percentages; 46 | } 47 | 48 | const CGFloat *angles() const { 49 | return _angles; 50 | } 51 | 52 | unsigned int count() const { 53 | return _sliceCount; 54 | } 55 | 56 | private: 57 | unsigned int _sliceCount; 58 | double *_percentages; 59 | double *_values; 60 | CGFloat *_angles; 61 | }; 62 | -------------------------------------------------------------------------------- /BTSPieChart/BTSSliceLayer.h: -------------------------------------------------------------------------------- 1 | // 2 | // BTSSliceLayer.h 3 | // 4 | // Copyright (c) 2011 Brian Coyner. All rights reserved. 5 | 6 | #import 7 | #import 8 | 9 | // Private implementation class. 10 | // 11 | // This layer represents a single slice. 12 | 13 | extern NSString *const kBTSSliceLayerAngle; 14 | 15 | @interface BTSSliceLayer : CAShapeLayer 16 | 17 | @property (nonatomic, assign, readwrite) CGFloat sliceAngle; 18 | 19 | + (id)layerWithColor:(CGColorRef)fillColor; 20 | 21 | @end 22 | -------------------------------------------------------------------------------- /BTSPieChart/BTSSliceLayer.m: -------------------------------------------------------------------------------- 1 | // 2 | // BTSSliceLayer.m 3 | // 4 | // Copyright (c) 2011 Brian Coyner. All rights reserved. 5 | 6 | #import "BTSSliceLayer.h" 7 | 8 | #import 9 | 10 | NSString *const kBTSSliceLayerAngle = @"sliceAngle"; 11 | 12 | @implementation BTSSliceLayer 13 | 14 | @dynamic sliceAngle; 15 | 16 | + (id)layerWithColor:(CGColorRef)fillColor 17 | { 18 | BTSSliceLayer *sliceLayer = [BTSSliceLayer layer]; 19 | [sliceLayer setFillColor:fillColor]; 20 | [sliceLayer setLineWidth:0.0]; 21 | [sliceLayer setContentsScale:[[UIScreen mainScreen] scale]]; 22 | 23 | // NOTE: the initial end angle is set to an out-of-range value. This is to ensure 24 | // that a new slice layer whose angle ends on 0.0 correctly fires KVO notifications 25 | // so that an animation takes place. 26 | [sliceLayer setSliceAngle:-1.0]; 27 | return sliceLayer; 28 | } 29 | 30 | @end 31 | -------------------------------------------------------------------------------- /BTSPieChart/MainStoryboard.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 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | -------------------------------------------------------------------------------- /BTSPieChart/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | -------------------------------------------------------------------------------- /BTSPieChart/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // BTSPieChart 4 | // 5 | // Created by Brian Coyner on 10/15/11. 6 | // Copyright (c) 2011 __MyCompanyName__. All rights reserved. 7 | // 8 | 9 | #import "BTSAppDelegate.h" 10 | 11 | int main(int argc, char *argv[]) { 12 | @autoreleasepool { 13 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([BTSAppDelegate class])); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ## Core Animation Pie Chart 2 | 3 | ### Created by Brian Coyner (2011, 2012, 2013, 2017) 4 | 5 | This demo was created to help support one of my [CocoaConf talks](http://cocoaconf.com/session/details/70). The demo 6 | shows various techniques for building an interactive, animatable pie chart using Core Animation (`CAShapeLayer`, `CATextLayer`, `CADisplayLink`, etc.) 7 | At the root of the solution is using a dynamic `CALayer` property to control the custom animations. The same technique can be applied to other solutions. 8 | 9 | ### Requirements/ Features 10 | 11 | - Xcode 8.3, iOS 10.3 12 | 13 | The pie chart contains theses features: 14 | 15 | - add new slices (animated) 16 | - remove selected slice (animated) 17 | - update existing pie values (animated) 18 | - interactive slice selection (tap and/ or move your finger) 19 | 20 | The view uses a data source (number of slices, slice value) and delegate (selection tracking). 21 | 22 | 23 | --------------------------------------------------------------------------------