├── .gitignore ├── .gitmodules ├── LICENSE ├── Modal Notification Controller.xcodeproj └── project.pbxproj ├── Modal Notification Controller ├── AppDelegate.swift ├── Base.lproj │ └── Main.storyboard ├── ButtonViewController.swift ├── Images.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── LaunchImage.launchimage │ │ └── Contents.json ├── Info.plist ├── PhotoViewController.swift ├── TextViewController.swift ├── ViewController.swift └── swan.jpg ├── Modal Notification ControllerTests ├── Info.plist └── Modal_Notification_ControllerTests.swift ├── ModalNotificationAnimator.swift ├── ModalNotificationViewController.swift └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | .DS_Store 3 | build/ 4 | *.pbxuser 5 | !default.pbxuser 6 | *.mode1v3 7 | !default.mode1v3 8 | *.mode2v3 9 | !default.mode2v3 10 | *.perspectivev3 11 | !default.perspectivev3 12 | *.xcworkspace 13 | !default.xcworkspace 14 | xcuserdata 15 | profile 16 | *.moved-aside 17 | DerivedData 18 | .idea/ 19 | Pods 20 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "ScalarArithmetic"] 2 | path = ScalarArithmetic 3 | url = git@github.com:seivan/ScalarArithmetic.git 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Ash Furrow 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. -------------------------------------------------------------------------------- /Modal Notification Controller.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 5E0D5EBF19694B19005E5D42 /* PhotoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E0D5EBE19694B19005E5D42 /* PhotoViewController.swift */; }; 11 | 5E0D5EC119694D9F005E5D42 /* swan.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 5E0D5EC019694D9F005E5D42 /* swan.jpg */; }; 12 | 5E4522EF1968454C003661FC /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E4522EE1968454C003661FC /* AppDelegate.swift */; }; 13 | 5E4522F11968454C003661FC /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E4522F01968454C003661FC /* ViewController.swift */; }; 14 | 5E4522F41968454C003661FC /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 5E4522F21968454C003661FC /* Main.storyboard */; }; 15 | 5E4522F61968454C003661FC /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5E4522F51968454C003661FC /* Images.xcassets */; }; 16 | 5E4523021968454C003661FC /* Modal_Notification_ControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E4523011968454C003661FC /* Modal_Notification_ControllerTests.swift */; }; 17 | 5E45230C196846B3003661FC /* ModalNotificationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E45230B196846B3003661FC /* ModalNotificationViewController.swift */; }; 18 | 5E45230E19684804003661FC /* ModalNotificationAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E45230D19684804003661FC /* ModalNotificationAnimator.swift */; }; 19 | 5E452311196868AF003661FC /* ScalarArithmetic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E452310196868AF003661FC /* ScalarArithmetic.swift */; }; 20 | 5E49B23C1969519000269808 /* TextViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E49B23B1969519000269808 /* TextViewController.swift */; }; 21 | 5E49B23E1969525300269808 /* ButtonViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E49B23D1969525300269808 /* ButtonViewController.swift */; }; 22 | /* End PBXBuildFile section */ 23 | 24 | /* Begin PBXContainerItemProxy section */ 25 | 5E4522FC1968454C003661FC /* PBXContainerItemProxy */ = { 26 | isa = PBXContainerItemProxy; 27 | containerPortal = 5E4522E11968454C003661FC /* Project object */; 28 | proxyType = 1; 29 | remoteGlobalIDString = 5E4522E81968454C003661FC; 30 | remoteInfo = "Modal Notification Controller"; 31 | }; 32 | /* End PBXContainerItemProxy section */ 33 | 34 | /* Begin PBXFileReference section */ 35 | 5E0D5EBE19694B19005E5D42 /* PhotoViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoViewController.swift; sourceTree = ""; }; 36 | 5E0D5EC019694D9F005E5D42 /* swan.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = swan.jpg; sourceTree = ""; }; 37 | 5E4522E91968454C003661FC /* Modal Notification Controller.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Modal Notification Controller.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 38 | 5E4522ED1968454C003661FC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 39 | 5E4522EE1968454C003661FC /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 40 | 5E4522F01968454C003661FC /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 41 | 5E4522F31968454C003661FC /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 42 | 5E4522F51968454C003661FC /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 43 | 5E4522FB1968454C003661FC /* Modal Notification ControllerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Modal Notification ControllerTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 44 | 5E4523001968454C003661FC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 45 | 5E4523011968454C003661FC /* Modal_Notification_ControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modal_Notification_ControllerTests.swift; sourceTree = ""; }; 46 | 5E45230B196846B3003661FC /* ModalNotificationViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ModalNotificationViewController.swift; sourceTree = ""; }; 47 | 5E45230D19684804003661FC /* ModalNotificationAnimator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ModalNotificationAnimator.swift; sourceTree = ""; }; 48 | 5E452310196868AF003661FC /* ScalarArithmetic.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScalarArithmetic.swift; sourceTree = ""; }; 49 | 5E49B23B1969519000269808 /* TextViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextViewController.swift; sourceTree = ""; }; 50 | 5E49B23D1969525300269808 /* ButtonViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ButtonViewController.swift; sourceTree = ""; }; 51 | /* End PBXFileReference section */ 52 | 53 | /* Begin PBXFrameworksBuildPhase section */ 54 | 5E4522E61968454C003661FC /* Frameworks */ = { 55 | isa = PBXFrameworksBuildPhase; 56 | buildActionMask = 2147483647; 57 | files = ( 58 | ); 59 | runOnlyForDeploymentPostprocessing = 0; 60 | }; 61 | 5E4522F81968454C003661FC /* Frameworks */ = { 62 | isa = PBXFrameworksBuildPhase; 63 | buildActionMask = 2147483647; 64 | files = ( 65 | ); 66 | runOnlyForDeploymentPostprocessing = 0; 67 | }; 68 | /* End PBXFrameworksBuildPhase section */ 69 | 70 | /* Begin PBXGroup section */ 71 | 5E4522E01968454C003661FC = { 72 | isa = PBXGroup; 73 | children = ( 74 | 5E45230B196846B3003661FC /* ModalNotificationViewController.swift */, 75 | 5E45230D19684804003661FC /* ModalNotificationAnimator.swift */, 76 | 5E4522EB1968454C003661FC /* Modal Notification Controller */, 77 | 5E45230F196868AF003661FC /* ScalarArithmetic */, 78 | 5E4522FE1968454C003661FC /* Modal Notification ControllerTests */, 79 | 5E4522EA1968454C003661FC /* Products */, 80 | ); 81 | sourceTree = ""; 82 | }; 83 | 5E4522EA1968454C003661FC /* Products */ = { 84 | isa = PBXGroup; 85 | children = ( 86 | 5E4522E91968454C003661FC /* Modal Notification Controller.app */, 87 | 5E4522FB1968454C003661FC /* Modal Notification ControllerTests.xctest */, 88 | ); 89 | name = Products; 90 | sourceTree = ""; 91 | }; 92 | 5E4522EB1968454C003661FC /* Modal Notification Controller */ = { 93 | isa = PBXGroup; 94 | children = ( 95 | 5E4522EE1968454C003661FC /* AppDelegate.swift */, 96 | 5E4522F01968454C003661FC /* ViewController.swift */, 97 | 5E0D5EBE19694B19005E5D42 /* PhotoViewController.swift */, 98 | 5E0D5EC019694D9F005E5D42 /* swan.jpg */, 99 | 5E49B23B1969519000269808 /* TextViewController.swift */, 100 | 5E49B23D1969525300269808 /* ButtonViewController.swift */, 101 | 5E4522F21968454C003661FC /* Main.storyboard */, 102 | 5E4522F51968454C003661FC /* Images.xcassets */, 103 | 5E4522EC1968454C003661FC /* Supporting Files */, 104 | ); 105 | path = "Modal Notification Controller"; 106 | sourceTree = ""; 107 | }; 108 | 5E4522EC1968454C003661FC /* Supporting Files */ = { 109 | isa = PBXGroup; 110 | children = ( 111 | 5E4522ED1968454C003661FC /* Info.plist */, 112 | ); 113 | name = "Supporting Files"; 114 | sourceTree = ""; 115 | }; 116 | 5E4522FE1968454C003661FC /* Modal Notification ControllerTests */ = { 117 | isa = PBXGroup; 118 | children = ( 119 | 5E4523011968454C003661FC /* Modal_Notification_ControllerTests.swift */, 120 | 5E4522FF1968454C003661FC /* Supporting Files */, 121 | ); 122 | path = "Modal Notification ControllerTests"; 123 | sourceTree = ""; 124 | }; 125 | 5E4522FF1968454C003661FC /* Supporting Files */ = { 126 | isa = PBXGroup; 127 | children = ( 128 | 5E4523001968454C003661FC /* Info.plist */, 129 | ); 130 | name = "Supporting Files"; 131 | sourceTree = ""; 132 | }; 133 | 5E45230F196868AF003661FC /* ScalarArithmetic */ = { 134 | isa = PBXGroup; 135 | children = ( 136 | 5E452310196868AF003661FC /* ScalarArithmetic.swift */, 137 | ); 138 | name = ScalarArithmetic; 139 | path = ScalarArithmetic/ScalarArithmetic; 140 | sourceTree = ""; 141 | }; 142 | /* End PBXGroup section */ 143 | 144 | /* Begin PBXNativeTarget section */ 145 | 5E4522E81968454C003661FC /* Modal Notification Controller */ = { 146 | isa = PBXNativeTarget; 147 | buildConfigurationList = 5E4523051968454C003661FC /* Build configuration list for PBXNativeTarget "Modal Notification Controller" */; 148 | buildPhases = ( 149 | 5E4522E51968454C003661FC /* Sources */, 150 | 5E4522E61968454C003661FC /* Frameworks */, 151 | 5E4522E71968454C003661FC /* Resources */, 152 | ); 153 | buildRules = ( 154 | ); 155 | dependencies = ( 156 | ); 157 | name = "Modal Notification Controller"; 158 | productName = "Modal Notification Controller"; 159 | productReference = 5E4522E91968454C003661FC /* Modal Notification Controller.app */; 160 | productType = "com.apple.product-type.application"; 161 | }; 162 | 5E4522FA1968454C003661FC /* Modal Notification ControllerTests */ = { 163 | isa = PBXNativeTarget; 164 | buildConfigurationList = 5E4523081968454C003661FC /* Build configuration list for PBXNativeTarget "Modal Notification ControllerTests" */; 165 | buildPhases = ( 166 | 5E4522F71968454C003661FC /* Sources */, 167 | 5E4522F81968454C003661FC /* Frameworks */, 168 | 5E4522F91968454C003661FC /* Resources */, 169 | ); 170 | buildRules = ( 171 | ); 172 | dependencies = ( 173 | 5E4522FD1968454C003661FC /* PBXTargetDependency */, 174 | ); 175 | name = "Modal Notification ControllerTests"; 176 | productName = "Modal Notification ControllerTests"; 177 | productReference = 5E4522FB1968454C003661FC /* Modal Notification ControllerTests.xctest */; 178 | productType = "com.apple.product-type.bundle.unit-test"; 179 | }; 180 | /* End PBXNativeTarget section */ 181 | 182 | /* Begin PBXProject section */ 183 | 5E4522E11968454C003661FC /* Project object */ = { 184 | isa = PBXProject; 185 | attributes = { 186 | LastUpgradeCheck = 0600; 187 | ORGANIZATIONNAME = "Ash Furrow"; 188 | TargetAttributes = { 189 | 5E4522E81968454C003661FC = { 190 | CreatedOnToolsVersion = 6.0; 191 | }; 192 | 5E4522FA1968454C003661FC = { 193 | CreatedOnToolsVersion = 6.0; 194 | TestTargetID = 5E4522E81968454C003661FC; 195 | }; 196 | }; 197 | }; 198 | buildConfigurationList = 5E4522E41968454C003661FC /* Build configuration list for PBXProject "Modal Notification Controller" */; 199 | compatibilityVersion = "Xcode 3.2"; 200 | developmentRegion = English; 201 | hasScannedForEncodings = 0; 202 | knownRegions = ( 203 | en, 204 | Base, 205 | ); 206 | mainGroup = 5E4522E01968454C003661FC; 207 | productRefGroup = 5E4522EA1968454C003661FC /* Products */; 208 | projectDirPath = ""; 209 | projectRoot = ""; 210 | targets = ( 211 | 5E4522E81968454C003661FC /* Modal Notification Controller */, 212 | 5E4522FA1968454C003661FC /* Modal Notification ControllerTests */, 213 | ); 214 | }; 215 | /* End PBXProject section */ 216 | 217 | /* Begin PBXResourcesBuildPhase section */ 218 | 5E4522E71968454C003661FC /* Resources */ = { 219 | isa = PBXResourcesBuildPhase; 220 | buildActionMask = 2147483647; 221 | files = ( 222 | 5E4522F41968454C003661FC /* Main.storyboard in Resources */, 223 | 5E4522F61968454C003661FC /* Images.xcassets in Resources */, 224 | 5E0D5EC119694D9F005E5D42 /* swan.jpg in Resources */, 225 | ); 226 | runOnlyForDeploymentPostprocessing = 0; 227 | }; 228 | 5E4522F91968454C003661FC /* Resources */ = { 229 | isa = PBXResourcesBuildPhase; 230 | buildActionMask = 2147483647; 231 | files = ( 232 | ); 233 | runOnlyForDeploymentPostprocessing = 0; 234 | }; 235 | /* End PBXResourcesBuildPhase section */ 236 | 237 | /* Begin PBXSourcesBuildPhase section */ 238 | 5E4522E51968454C003661FC /* Sources */ = { 239 | isa = PBXSourcesBuildPhase; 240 | buildActionMask = 2147483647; 241 | files = ( 242 | 5E49B23E1969525300269808 /* ButtonViewController.swift in Sources */, 243 | 5E4522F11968454C003661FC /* ViewController.swift in Sources */, 244 | 5E45230E19684804003661FC /* ModalNotificationAnimator.swift in Sources */, 245 | 5E452311196868AF003661FC /* ScalarArithmetic.swift in Sources */, 246 | 5E45230C196846B3003661FC /* ModalNotificationViewController.swift in Sources */, 247 | 5E4522EF1968454C003661FC /* AppDelegate.swift in Sources */, 248 | 5E49B23C1969519000269808 /* TextViewController.swift in Sources */, 249 | 5E0D5EBF19694B19005E5D42 /* PhotoViewController.swift in Sources */, 250 | ); 251 | runOnlyForDeploymentPostprocessing = 0; 252 | }; 253 | 5E4522F71968454C003661FC /* Sources */ = { 254 | isa = PBXSourcesBuildPhase; 255 | buildActionMask = 2147483647; 256 | files = ( 257 | 5E4523021968454C003661FC /* Modal_Notification_ControllerTests.swift in Sources */, 258 | ); 259 | runOnlyForDeploymentPostprocessing = 0; 260 | }; 261 | /* End PBXSourcesBuildPhase section */ 262 | 263 | /* Begin PBXTargetDependency section */ 264 | 5E4522FD1968454C003661FC /* PBXTargetDependency */ = { 265 | isa = PBXTargetDependency; 266 | target = 5E4522E81968454C003661FC /* Modal Notification Controller */; 267 | targetProxy = 5E4522FC1968454C003661FC /* PBXContainerItemProxy */; 268 | }; 269 | /* End PBXTargetDependency section */ 270 | 271 | /* Begin PBXVariantGroup section */ 272 | 5E4522F21968454C003661FC /* Main.storyboard */ = { 273 | isa = PBXVariantGroup; 274 | children = ( 275 | 5E4522F31968454C003661FC /* Base */, 276 | ); 277 | name = Main.storyboard; 278 | sourceTree = ""; 279 | }; 280 | /* End PBXVariantGroup section */ 281 | 282 | /* Begin XCBuildConfiguration section */ 283 | 5E4523031968454C003661FC /* Debug */ = { 284 | isa = XCBuildConfiguration; 285 | buildSettings = { 286 | ALWAYS_SEARCH_USER_PATHS = NO; 287 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 288 | CLANG_CXX_LIBRARY = "libc++"; 289 | CLANG_ENABLE_MODULES = YES; 290 | CLANG_ENABLE_OBJC_ARC = YES; 291 | CLANG_WARN_BOOL_CONVERSION = YES; 292 | CLANG_WARN_CONSTANT_CONVERSION = YES; 293 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 294 | CLANG_WARN_EMPTY_BODY = YES; 295 | CLANG_WARN_ENUM_CONVERSION = YES; 296 | CLANG_WARN_INT_CONVERSION = YES; 297 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 298 | CLANG_WARN_UNREACHABLE_CODE = YES; 299 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 300 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 301 | COPY_PHASE_STRIP = NO; 302 | ENABLE_STRICT_OBJC_MSGSEND = YES; 303 | GCC_C_LANGUAGE_STANDARD = gnu99; 304 | GCC_DYNAMIC_NO_PIC = NO; 305 | GCC_OPTIMIZATION_LEVEL = 0; 306 | GCC_PREPROCESSOR_DEFINITIONS = ( 307 | "DEBUG=1", 308 | "$(inherited)", 309 | ); 310 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 311 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 312 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 313 | GCC_WARN_UNDECLARED_SELECTOR = YES; 314 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 315 | GCC_WARN_UNUSED_FUNCTION = YES; 316 | GCC_WARN_UNUSED_VARIABLE = YES; 317 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 318 | METAL_ENABLE_DEBUG_INFO = YES; 319 | ONLY_ACTIVE_ARCH = YES; 320 | SDKROOT = iphoneos; 321 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 322 | }; 323 | name = Debug; 324 | }; 325 | 5E4523041968454C003661FC /* Release */ = { 326 | isa = XCBuildConfiguration; 327 | buildSettings = { 328 | ALWAYS_SEARCH_USER_PATHS = NO; 329 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 330 | CLANG_CXX_LIBRARY = "libc++"; 331 | CLANG_ENABLE_MODULES = YES; 332 | CLANG_ENABLE_OBJC_ARC = YES; 333 | CLANG_WARN_BOOL_CONVERSION = YES; 334 | CLANG_WARN_CONSTANT_CONVERSION = YES; 335 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 336 | CLANG_WARN_EMPTY_BODY = YES; 337 | CLANG_WARN_ENUM_CONVERSION = YES; 338 | CLANG_WARN_INT_CONVERSION = YES; 339 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 340 | CLANG_WARN_UNREACHABLE_CODE = YES; 341 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 342 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 343 | COPY_PHASE_STRIP = YES; 344 | ENABLE_NS_ASSERTIONS = NO; 345 | ENABLE_STRICT_OBJC_MSGSEND = YES; 346 | GCC_C_LANGUAGE_STANDARD = gnu99; 347 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 348 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 349 | GCC_WARN_UNDECLARED_SELECTOR = YES; 350 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 351 | GCC_WARN_UNUSED_FUNCTION = YES; 352 | GCC_WARN_UNUSED_VARIABLE = YES; 353 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 354 | METAL_ENABLE_DEBUG_INFO = NO; 355 | SDKROOT = iphoneos; 356 | VALIDATE_PRODUCT = YES; 357 | }; 358 | name = Release; 359 | }; 360 | 5E4523061968454C003661FC /* Debug */ = { 361 | isa = XCBuildConfiguration; 362 | buildSettings = { 363 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 364 | ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; 365 | INFOPLIST_FILE = "Modal Notification Controller/Info.plist"; 366 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 367 | PRODUCT_NAME = "$(TARGET_NAME)"; 368 | }; 369 | name = Debug; 370 | }; 371 | 5E4523071968454C003661FC /* Release */ = { 372 | isa = XCBuildConfiguration; 373 | buildSettings = { 374 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 375 | ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; 376 | INFOPLIST_FILE = "Modal Notification Controller/Info.plist"; 377 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 378 | PRODUCT_NAME = "$(TARGET_NAME)"; 379 | }; 380 | name = Release; 381 | }; 382 | 5E4523091968454C003661FC /* Debug */ = { 383 | isa = XCBuildConfiguration; 384 | buildSettings = { 385 | BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/Modal Notification Controller.app/Modal Notification Controller"; 386 | FRAMEWORK_SEARCH_PATHS = ( 387 | "$(SDKROOT)/Developer/Library/Frameworks", 388 | "$(inherited)", 389 | ); 390 | GCC_PREPROCESSOR_DEFINITIONS = ( 391 | "DEBUG=1", 392 | "$(inherited)", 393 | ); 394 | INFOPLIST_FILE = "Modal Notification ControllerTests/Info.plist"; 395 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 396 | METAL_ENABLE_DEBUG_INFO = YES; 397 | PRODUCT_NAME = "$(TARGET_NAME)"; 398 | TEST_HOST = "$(BUNDLE_LOADER)"; 399 | }; 400 | name = Debug; 401 | }; 402 | 5E45230A1968454C003661FC /* Release */ = { 403 | isa = XCBuildConfiguration; 404 | buildSettings = { 405 | BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/Modal Notification Controller.app/Modal Notification Controller"; 406 | FRAMEWORK_SEARCH_PATHS = ( 407 | "$(SDKROOT)/Developer/Library/Frameworks", 408 | "$(inherited)", 409 | ); 410 | INFOPLIST_FILE = "Modal Notification ControllerTests/Info.plist"; 411 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 412 | METAL_ENABLE_DEBUG_INFO = NO; 413 | PRODUCT_NAME = "$(TARGET_NAME)"; 414 | TEST_HOST = "$(BUNDLE_LOADER)"; 415 | }; 416 | name = Release; 417 | }; 418 | /* End XCBuildConfiguration section */ 419 | 420 | /* Begin XCConfigurationList section */ 421 | 5E4522E41968454C003661FC /* Build configuration list for PBXProject "Modal Notification Controller" */ = { 422 | isa = XCConfigurationList; 423 | buildConfigurations = ( 424 | 5E4523031968454C003661FC /* Debug */, 425 | 5E4523041968454C003661FC /* Release */, 426 | ); 427 | defaultConfigurationIsVisible = 0; 428 | defaultConfigurationName = Release; 429 | }; 430 | 5E4523051968454C003661FC /* Build configuration list for PBXNativeTarget "Modal Notification Controller" */ = { 431 | isa = XCConfigurationList; 432 | buildConfigurations = ( 433 | 5E4523061968454C003661FC /* Debug */, 434 | 5E4523071968454C003661FC /* Release */, 435 | ); 436 | defaultConfigurationIsVisible = 0; 437 | defaultConfigurationName = Release; 438 | }; 439 | 5E4523081968454C003661FC /* Build configuration list for PBXNativeTarget "Modal Notification ControllerTests" */ = { 440 | isa = XCConfigurationList; 441 | buildConfigurations = ( 442 | 5E4523091968454C003661FC /* Debug */, 443 | 5E45230A1968454C003661FC /* Release */, 444 | ); 445 | defaultConfigurationIsVisible = 0; 446 | defaultConfigurationName = Release; 447 | }; 448 | /* End XCConfigurationList section */ 449 | }; 450 | rootObject = 5E4522E11968454C003661FC /* Project object */; 451 | } 452 | -------------------------------------------------------------------------------- /Modal Notification Controller/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Modal Notification Controller 4 | // 5 | // Created by Ash Furrow on 2014-07-05. 6 | // Copyright (c) 2014 Ash Furrow. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: NSDictionary?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(application: UIApplication) { 23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 24 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(application: UIApplication) { 28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(application: UIApplication) { 33 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | func applicationDidBecomeActive(application: UIApplication) { 37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 38 | } 39 | 40 | func applicationWillTerminate(application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /Modal Notification Controller/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /Modal Notification Controller/ButtonViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ButtonViewController.swift 3 | // Modal Notification Controller 4 | // 5 | // Created by Ash Furrow on 2014-07-06. 6 | // Copyright (c) 2014 Ash Furrow. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ButtonViewController: UIViewController { 12 | override func viewDidLoad() { 13 | super.viewDidLoad() 14 | 15 | self.view.backgroundColor = UIColor.purpleColor() 16 | 17 | var button = UIButton.buttonWithType(.System) as UIButton 18 | button.setTitle("Press me", forState: .Normal) 19 | button.setTitleColor(UIColor.whiteColor(), forState: .Normal) 20 | button.addTarget(self, action: "buttonWasPressed:", forControlEvents: .TouchUpInside) 21 | button.center = self.view.center 22 | button.bounds = CGRectMake(0, 0, 100, 100) 23 | self.view.addSubview(button) 24 | } 25 | 26 | func buttonWasPressed(button: UIButton) { 27 | var alert = UIAlertController(title: "You Did It!", message: "You clicked me!", preferredStyle: .Alert) 28 | alert.addAction(UIAlertAction(title: "OK", style: .Default, handler: nil)) 29 | self.presentViewController(alert, animated: true, completion: nil) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Modal Notification Controller/Images.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" : "40x40", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "60x60", 16 | "scale" : "2x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /Modal Notification Controller/Images.xcassets/LaunchImage.launchimage/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "orientation" : "portrait", 5 | "idiom" : "iphone", 6 | "extent" : "full-screen", 7 | "minimum-system-version" : "7.0", 8 | "scale" : "2x" 9 | }, 10 | { 11 | "orientation" : "portrait", 12 | "idiom" : "iphone", 13 | "subtype" : "retina4", 14 | "extent" : "full-screen", 15 | "minimum-system-version" : "7.0", 16 | "scale" : "2x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /Modal Notification Controller/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | com.ashfurrow.${PRODUCT_NAME:rfc1034identifier} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UIStatusBarHidden 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /Modal Notification Controller/PhotoViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhotoViewController.swift 3 | // Modal Notification Controller 4 | // 5 | // Created by Ash Furrow on 2014-07-06. 6 | // Copyright (c) 2014 Ash Furrow. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class PhotoViewController: UIViewController { 12 | override func viewDidLoad() { 13 | super.viewDidLoad() 14 | 15 | let imageView = UIImageView(image: UIImage(named: "swan.jpg")) 16 | imageView.frame = self.view.bounds 17 | imageView.contentMode = .ScaleAspectFill 18 | self.view.addSubview(imageView) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Modal Notification Controller/TextViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TextViewController.swift 3 | // Modal Notification Controller 4 | // 5 | // Created by Ash Furrow on 2014-07-06. 6 | // Copyright (c) 2014 Ash Furrow. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class TextViewController: UIViewController { 12 | override func viewDidLoad() { 13 | super.viewDidLoad() 14 | 15 | self.view.backgroundColor = UIColor.whiteColor() 16 | 17 | let label = UILabel(frame: CGRectInset(self.view.bounds, 10, 10)) 18 | label.numberOfLines = 0 19 | label.lineBreakMode = .ByWordWrapping 20 | label.text = "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." 21 | self.view.addSubview(label) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Modal Notification Controller/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Modal Notification Controller 4 | // 5 | // Created by Ash Furrow on 2014-07-05. 6 | // Copyright (c) 2014 Ash Furrow. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ViewController: UIViewController, UIViewControllerTransitioningDelegate, ModalNotificationViewControllerDelegate { 12 | @IBAction func presentButtonWasPressed(sender: UIButton) { 13 | let viewController = ModalNotificationViewController(delegate: self) 14 | viewController.transitioningDelegate = self 15 | viewController.modalPresentationStyle = .Custom 16 | presentViewController(viewController, animated: true, completion: nil) 17 | } 18 | 19 | func animationControllerForPresentedController(presented: UIViewController!, presentingController presenting: UIViewController!, sourceController source: UIViewController!) -> UIViewControllerAnimatedTransitioning! { 20 | return ModalNotificationAnimator(presenting: true) 21 | } 22 | 23 | func animationControllerForDismissedController(dismissed: UIViewController!) -> UIViewControllerAnimatedTransitioning! { 24 | return ModalNotificationAnimator() 25 | } 26 | 27 | func numberOfViewControllers() -> Int { 28 | return 3 29 | } 30 | 31 | func viewControllerAtIndex(index: UInt) -> UIViewController { 32 | switch index { 33 | case 0: 34 | return PhotoViewController() 35 | case 1: 36 | return TextViewController() 37 | default: 38 | return ButtonViewController() 39 | } 40 | } 41 | 42 | override func prefersStatusBarHidden() -> Bool { 43 | return true 44 | } 45 | } 46 | 47 | -------------------------------------------------------------------------------- /Modal Notification Controller/swan.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashfurrow/ModalNotificationController/be68a9789f299c871521af3e145b715aedb88540/Modal Notification Controller/swan.jpg -------------------------------------------------------------------------------- /Modal Notification ControllerTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | com.ashfurrow.${PRODUCT_NAME:rfc1034identifier} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /Modal Notification ControllerTests/Modal_Notification_ControllerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Modal_Notification_ControllerTests.swift 3 | // Modal Notification ControllerTests 4 | // 5 | // Created by Ash Furrow on 2014-07-05. 6 | // Copyright (c) 2014 Ash Furrow. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | class Modal_Notification_ControllerTests: XCTestCase { 12 | 13 | override func setUp() { 14 | super.setUp() 15 | // Put setup code here. This method is called before the invocation of each test method in the class. 16 | } 17 | 18 | override func tearDown() { 19 | // Put teardown code here. This method is called after the invocation of each test method in the class. 20 | super.tearDown() 21 | } 22 | 23 | func testExample() { 24 | // This is an example of a functional test case. 25 | XCTAssert(true, "Pass") 26 | } 27 | 28 | func testPerformanceExample() { 29 | // This is an example of a performance test case. 30 | self.measureBlock() { 31 | // Put the code you want to measure the time of here. 32 | } 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /ModalNotificationAnimator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ModalNotificationAnimator.swift 3 | // Modal Notification Controller 4 | // 5 | // Created by Ash Furrow on 2014-07-05. 6 | // Copyright (c) 2014 Ash Furrow. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ModalNotificationAnimator: NSObject, UIViewControllerAnimatedTransitioning { 12 | let presenting: Bool 13 | 14 | init(presenting: Bool = false) { 15 | self.presenting = presenting 16 | super.init() 17 | } 18 | 19 | func transitionDuration(transitionContext: UIViewControllerContextTransitioning!) -> NSTimeInterval { 20 | return 0.3 21 | } 22 | 23 | func animateTransition(transitionContext: UIViewControllerContextTransitioning!) { 24 | let fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)! 25 | let toViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)! 26 | let containerView = transitionContext.containerView() 27 | 28 | if (presenting) { 29 | fromViewController.view.userInteractionEnabled = false 30 | 31 | containerView.backgroundColor = UIColor.blackColor() 32 | 33 | toViewController.view.alpha = 0.0 34 | toViewController.view.transform = CGAffineTransformMakeScale(0.1, 0.1) 35 | 36 | containerView.addSubview(fromViewController.view) 37 | containerView.addSubview(toViewController.view) 38 | 39 | UIView.animateWithDuration(self.transitionDuration(transitionContext), animations: { 40 | toViewController.view.transform = CGAffineTransformIdentity 41 | toViewController.view.alpha = 1.0 42 | fromViewController.view.alpha = 0.5 43 | }, completion: { (value: Bool) in 44 | transitionContext.completeTransition(true) 45 | }) 46 | } else { 47 | toViewController.view.userInteractionEnabled = true 48 | 49 | containerView.addSubview(toViewController.view) 50 | containerView.addSubview(fromViewController.view) 51 | 52 | UIView.animateWithDuration(self.transitionDuration(transitionContext), animations: { 53 | toViewController.view.alpha = 1.0 54 | fromViewController.view.alpha = 0.0 55 | }, completion: { (value: Bool) in 56 | transitionContext.completeTransition(true) 57 | // This following line is to work around a bug in the iOS 8 beta (2) 58 | UIApplication.sharedApplication().keyWindow!.addSubview(toViewController.view) 59 | }) 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /ModalNotificationViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ModalNotificationViewController.swift 3 | // Modal Notification Controller 4 | // 5 | // Created by Ash Furrow on 2014-07-05. 6 | // Copyright (c) 2014 Ash Furrow. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @objc protocol ModalNotificationViewControllerDelegate: NSObjectProtocol { 12 | func numberOfViewControllers () -> Int 13 | func viewControllerAtIndex(index: UInt) -> UIViewController 14 | } 15 | 16 | class ModalNotificationViewController: UIViewController { 17 | weak var delegate: ModalNotificationViewControllerDelegate? 18 | @lazy var animator: UIDynamicAnimator = { 19 | let animator = UIDynamicAnimator(referenceView: self.view) 20 | return animator 21 | }() 22 | @lazy var panRecognizer: UIPanGestureRecognizer = { 23 | let panRecognizer = UIPanGestureRecognizer(target: self, action: "userDidPan:") 24 | return panRecognizer 25 | }() 26 | var snapBehaviour: UISnapBehavior? 27 | var pushBehaviour: UIPushBehavior? 28 | var attachmentBehaviour: UIAttachmentBehavior? 29 | var itemBehaviour: UIDynamicItemBehavior? 30 | var currentViewController: UIViewController? 31 | var index: UInt { 32 | didSet { 33 | checkForLastViewController() 34 | } 35 | } 36 | 37 | init(delegate: ModalNotificationViewControllerDelegate) { 38 | assert(delegate.numberOfViewControllers() > 0, "Number of view controllers must be at least one") 39 | self.delegate = delegate 40 | index = 0 41 | super.init(nibName: nil, bundle: nil) 42 | } 43 | 44 | override func viewDidLoad() { 45 | super.viewDidLoad() 46 | self.view.backgroundColor = UIColor.clearColor() 47 | addNextViewController() 48 | } 49 | 50 | override func viewDidAppear(animated: Bool) { 51 | super.viewDidAppear(animated) 52 | self.view.backgroundColor = UIColor.blackColor() 53 | } 54 | 55 | func addNextViewController(animated: Bool = false) { 56 | if let currentViewController = currentViewController { 57 | currentViewController.willMoveToParentViewController(nil) 58 | currentViewController.removeFromParentViewController() 59 | currentViewController.view.removeFromSuperview() 60 | currentViewController.didMoveToParentViewController(nil) 61 | removeBehavioursForCurrentController() 62 | } 63 | 64 | currentViewController = delegate?.viewControllerAtIndex(index) 65 | 66 | if let currentViewController = currentViewController { 67 | // Add to self 68 | currentViewController.willMoveToParentViewController(self) 69 | addChildViewController(currentViewController) 70 | view.addSubview(currentViewController.view) 71 | currentViewController.didMoveToParentViewController(self) 72 | addBehavioursToCurrentController() 73 | 74 | if (animated) { 75 | currentViewController.view.alpha = 0.0 76 | UIView.animateWithDuration(0.3, animations: { 77 | currentViewController.view.alpha = 1.0 78 | }) 79 | } 80 | } 81 | 82 | index++ 83 | } 84 | 85 | func removeBehavioursForCurrentController() { 86 | animator.removeBehavior(snapBehaviour) 87 | snapBehaviour = nil 88 | animator.removeBehavior(pushBehaviour) 89 | pushBehaviour = nil 90 | } 91 | 92 | func addBehavioursToCurrentController() { 93 | if let currentViewController = currentViewController { 94 | snapBehaviour = UISnapBehavior(item: currentViewController.view, snapToPoint: self.view.center) 95 | animator.addBehavior(snapBehaviour) 96 | 97 | pushBehaviour = UIPushBehavior(items: [currentViewController.view], mode: .Instantaneous) 98 | pushBehaviour!.magnitude = 0 99 | pushBehaviour!.angle = 0 100 | animator.addBehavior(pushBehaviour) 101 | 102 | itemBehaviour = UIDynamicItemBehavior(items: [currentViewController.view]) 103 | animator.addBehavior(itemBehaviour) 104 | 105 | currentViewController.view.addGestureRecognizer(panRecognizer) 106 | } 107 | } 108 | 109 | func checkForLastViewController() { 110 | if index == delegate!.numberOfViewControllers() + 1 { 111 | self.presentingViewController?.dismissViewControllerAnimated(true, completion: nil) 112 | } 113 | } 114 | 115 | func userDidPan(panRecognizer: UIPanGestureRecognizer) { 116 | // Many thanks to https://github.com/u10int/URBMediaFocusViewController 117 | let location = panRecognizer.locationInView(view) 118 | let boxLocation = panRecognizer.locationInView(currentViewController!.view) 119 | 120 | if (panRecognizer.state == .Began) { 121 | animator.removeBehavior(snapBehaviour) 122 | animator.removeBehavior(pushBehaviour) 123 | let centerOffset = UIOffsetMake(boxLocation.x - CGRectGetMidX(currentViewController!.view.bounds), boxLocation.y - CGRectGetMidY(currentViewController!.view.bounds)) 124 | attachmentBehaviour = UIAttachmentBehavior(item: currentViewController!.view, offsetFromCenter: centerOffset, attachedToAnchor: location) 125 | animator.addBehavior(attachmentBehaviour) 126 | } else if (panRecognizer.state == .Changed) { 127 | attachmentBehaviour!.anchorPoint = location 128 | } else if (panRecognizer.state == .Ended) { 129 | animator.removeBehavior(attachmentBehaviour) 130 | attachmentBehaviour = nil 131 | 132 | let runningOnPad = UIDevice.currentDevice().userInterfaceIdiom == .Pad 133 | // need to scale velocity values to tame down physics on the iPad 134 | let deviceVelocityScale = runningOnPad ? 0.2 : 1.0 135 | let deviceAngularScale = runningOnPad ? 0.7 : 1.0 136 | // factor to increase delay before `dismissAfterPush` is called on iPad to account for more area to cover to disappear 137 | let deviceDismissDelay = runningOnPad ? 1.8 : 1.0 138 | let velocity = panRecognizer.velocityInView(view) 139 | let velocityAdjust = 10.0 * deviceVelocityScale 140 | 141 | 142 | let minimumVelocityRequiredForPush = 50.0 143 | 144 | let shouldDismiss = abs(velocity.x / velocityAdjust) > minimumVelocityRequiredForPush || abs(velocity.y / velocityAdjust) > minimumVelocityRequiredForPush 145 | if shouldDismiss { 146 | let offsetFromCenter = UIOffsetMake(boxLocation.x - CGRectGetMidX(currentViewController!.view.bounds), boxLocation.y - CGRectGetMidY(currentViewController!.view.bounds)) 147 | let radius = sqrtf(powf(Float(offsetFromCenter.horizontal), 2.0) + powf(Float(offsetFromCenter.vertical), 2.0)) 148 | let pushVelocity = sqrtf(powf(Float(velocity.x), 2.0) + powf(Float(velocity.y), 2.0)) 149 | 150 | // calculate angles needed for angular velocity formula 151 | let velocityAngle = atan2f(Float(velocity.y), Float(velocity.x)) 152 | var locationAngle = atan2f(Float(offsetFromCenter.vertical), Float(offsetFromCenter.horizontal)) 153 | if (locationAngle > 0) { 154 | locationAngle -= Float(M_PI * 2.0) 155 | } 156 | 157 | // angle (θ) is the angle between the push vector (V) and vector component parallel to radius, so it should always be positive 158 | let angle = fabsf(fabsf(velocityAngle) - fabsf(locationAngle)) 159 | // angular velocity formula: w = (abs(V) * sin(θ)) / abs(r) 160 | var angularVelocity = Float(fabsf((fabsf(pushVelocity) * sinf(angle)) / fabsf(radius))) 161 | 162 | // rotation direction is dependent upon which corner was pushed relative to the center of the view 163 | // when velocity.y is positive, pushes to the right of center rotate clockwise, left is counterclockwise 164 | var direction = Float((location.x < view.center.x) ? -1.0 : 1.0) 165 | // when y component of velocity is negative, reverse direction 166 | if (velocity.y < 0) { direction *= -1 } 167 | 168 | // amount of angular velocity should be relative to how close to the edge of the view the force originated 169 | // angular velocity is reduced the closer to the center the force is applied 170 | // for angular velocity: positive = clockwise, negative = counterclockwise 171 | let xRatioFromCenter = fabsf(Float(offsetFromCenter.horizontal)) / (Float(CGRectGetWidth(currentViewController!.view.frame)) / 2.0) 172 | let yRatioFromCetner = fabsf(Float(offsetFromCenter.vertical)) / (Float(CGRectGetHeight(currentViewController!.view.frame)) / 2.0) 173 | 174 | // apply device scale to angular velocity 175 | angularVelocity *= Float(deviceAngularScale) 176 | // adjust angular velocity based on distance from center, force applied farther towards the edges gets more spin 177 | angularVelocity *= (xRatioFromCenter + yRatioFromCetner) / 2.0 178 | 179 | itemBehaviour!.addAngularVelocity(CGFloat(angularVelocity * direction), forItem: currentViewController!.view) 180 | animator.addBehavior(pushBehaviour) 181 | pushBehaviour!.pushDirection = CGVectorMake(CGFloat(velocity.x / velocityAdjust), CGFloat(velocity.y / velocityAdjust)) 182 | pushBehaviour!.active = true 183 | 184 | let maximumDismissDelay: Float = 0.5 185 | let delay = CGFloat(maximumDismissDelay - (Float(pushVelocity) / 10000.0)) 186 | dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(delay * NSEC_PER_SEC)), dispatch_get_main_queue(), { 187 | self.addNextViewController(animated: true) 188 | }) 189 | 190 | } else { 191 | animator.addBehavior(snapBehaviour) 192 | animator.addBehavior(pushBehaviour) 193 | } 194 | } 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ModalNotificationController 2 | =========================== 3 | 4 | This is a demonstration of how to present Slingshot-style notification using Swift. Developed at the [SwiftCrunch](http://swiftcrunch.com) hackathon. 5 | 6 | The Goal 7 | ---------------- 8 | 9 | My goal is to recreate the [Facebook Slingshot](https://itunes.apple.com/ca/app/slingshot/id878681557?mt=8) notification UI. This is different from other designs in that it presents an arbitrary number of things that can be dismissed by "flicking" them offscreen. Here's what it looks like: 10 | 11 | ![Facebook Slingshot](http://f.cl.ly/items/1F3M2C1g1l1Z1A1p0D2S/goal.gif) 12 | 13 | OK, cool. So how do we do this? 14 | 15 | Approach 16 | ---------------- 17 | 18 | This project depends on the custom UIViewController transition API introduced in iOS 7 (this means it doesn't really work for landscape orientation :cry:). What's key is that I want to display *arbitrary content* in my notifications – not just images. 19 | 20 | After presenting the `ModalNotificationViewController`, that controller will query its delegate about the number of view controllers to be presented. Then, it will ask for each view controller in sequence. Once the view controllers have been exhausted, it will dismiss itself. 21 | 22 | Current Status 23 | ---------------- 24 | 25 | Mostly done. I'll turn it into a CocoaPod once [CocoaPods supports Swift](https://github.com/CocoaPods/CocoaPods/pull/2222). 26 | 27 | ![Screenshot](http://cloud.ashfurrow.com/image/2M2V1h3H2g0e/2014-07-06%2012_00_37.gif) 28 | 29 | How to Use 30 | ---------------- 31 | 32 | You need two files to use this library: `ModalNotificationAnimator.swift` and `ModalNotificationViewController.swift`. You also need [Scalar Arithmetic](https://github.com/seivan/ScalarArithmetic), at least for now until Apple fixes Swift. 33 | 34 | To use the library, you want to present the modal notification view controller, but it's not that simple. 35 | 36 | ```swift 37 | let viewController = ModalNotificationViewController(delegate: self) 38 | viewController.transitioningDelegate = self; 39 | viewController.modalPresentationStyle = .Custom; 40 | presentViewController(viewController, animated: true, completion: nil) 41 | ``` 42 | 43 | Here we're using a custom view controller transition. You'll need to conform to the `UIViewControllerTransitioningDelegate` protocol and implement these two methods: 44 | 45 | ```swift 46 | func animationControllerForPresentedController(presented: UIViewController!, presentingController presenting: UIViewController!, sourceController source: UIViewController!) -> UIViewControllerAnimatedTransitioning! { 47 | return ModalNotificationAnimator(presenting: true) 48 | } 49 | 50 | func animationControllerForDismissedController(dismissed: UIViewController!) -> UIViewControllerAnimatedTransitioning! { 51 | return ModalNotificationAnimator() 52 | } 53 | ``` 54 | 55 | You'll also need to provide a delegate to the modal view controller. In this example, it's `self`, so you need to conform to the `ModalNotificationViewControllerDelegate` protocol. That means you'll need to implement these two methods: 56 | 57 | ```swift 58 | func numberOfViewControllers() -> Int { 59 | return /* however many view controllers to present */ 60 | } 61 | 62 | func viewControllerAtIndex(index: UInt) -> UIViewController { 63 | /* vend a view controller to present */ 64 | } 65 | ``` 66 | 67 | Just be careful that the view controller you vend back doesn't have a conflicting gesture recognizer, or else you'll never be able to dismiss it! 68 | 69 | Workarounds 70 | ---------------- 71 | 72 | Two radars were filed in the making of this project (made on iOS 8 Beta 2): 73 | 74 | - [UIWindow view hierarchy disappears when dismissing view controller with custom presentation](http://openradar.appspot.com/radar?id=5320103646199808) 75 | - [Cannot reference unowned object conforming to protocol](http://openradar.appspot.com/radar?id=5300501415460864) 76 | 77 | Credits 78 | ---------------- 79 | 80 | My special thanks to [Nicholas Shipes](https://github.com/u10int) for his work on [URBMediaFocusViewController](https://github.com/u10int/URBMediaFocusViewController), which provided the base for my gesture recognizer code. 81 | 82 | Also thanks to [Seivan Heidari](https://github.com/seivan) for their work on [Scalar Arithmetic](https://github.com/seivan/ScalarArithmetic). 83 | 84 | License 85 | ---------------- 86 | 87 | Copyright (c) Ash Furrow, 2014 88 | 89 | Permission is hereby granted, free of charge, to any person obtaining a copy 90 | of this software and associated documentation files (the "Software"), to deal 91 | in the Software without restriction, including without limitation the rights 92 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 93 | copies of the Software, and to permit persons to whom the Software is 94 | furnished to do so, subject to the following conditions: 95 | 96 | The above copyright notice and this permission notice shall be included in 97 | all copies or substantial portions of the Software. 98 | 99 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 100 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 101 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 102 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 103 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 104 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 105 | THE SOFTWARE. 106 | --------------------------------------------------------------------------------