├── .gitignore ├── .travis.yml ├── LICENSE.md ├── README.md ├── ViewControllerLifecycleObservers.xcodeproj ├── project.pbxproj └── xcshareddata │ └── xcschemes │ ├── ViewControllerLifecycleObservers.xcscheme │ └── ViewControllerLifecycleObserversSampleApp.xcscheme ├── ViewControllerLifecycleObservers ├── Info.plist └── UIViewController+LifecycleObservers.swift ├── ViewControllerLifecycleObserversSampleApp ├── AppDelegate.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── Base.lproj │ └── LaunchScreen.storyboard └── Info.plist ├── ViewControllerLifecycleObserversTests ├── Info.plist └── ViewControllerLifecycleObserversTests.swift └── codecov.yml /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/swift,xcode 2 | 3 | ### Swift ### 4 | # Xcode 5 | # 6 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 7 | 8 | ## Build generated 9 | build/ 10 | DerivedData/ 11 | 12 | ## Various settings 13 | *.pbxuser 14 | !default.pbxuser 15 | *.mode1v3 16 | !default.mode1v3 17 | *.mode2v3 18 | !default.mode2v3 19 | *.perspectivev3 20 | !default.perspectivev3 21 | xcuserdata/ 22 | 23 | ## Other 24 | *.moved-aside 25 | *.xccheckout 26 | *.xcscmblueprint 27 | 28 | ## Obj-C/Swift specific 29 | *.hmap 30 | *.ipa 31 | *.dSYM.zip 32 | *.dSYM 33 | 34 | ## Playgrounds 35 | timeline.xctimeline 36 | playground.xcworkspace 37 | 38 | # Swift Package Manager 39 | # 40 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 41 | # Packages/ 42 | # Package.pins 43 | # Package.resolved 44 | .build/ 45 | 46 | # CocoaPods 47 | # 48 | # We recommend against adding the Pods directory to your .gitignore. However 49 | # you should judge for yourself, the pros and cons are mentioned at: 50 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 51 | # 52 | # Pods/ 53 | # 54 | # Add this line if you want to avoid checking in source code from the Xcode workspace 55 | # *.xcworkspace 56 | 57 | # Carthage 58 | # 59 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 60 | # Carthage/Checkouts 61 | 62 | Carthage/Build 63 | 64 | # fastlane 65 | # 66 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 67 | # screenshots whenever they are needed. 68 | # For more information about the recommended setup visit: 69 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 70 | 71 | fastlane/report.xml 72 | fastlane/Preview.html 73 | fastlane/screenshots/**/*.png 74 | fastlane/test_output 75 | 76 | ### Xcode ### 77 | # Xcode 78 | # 79 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 80 | 81 | ## User settings 82 | 83 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 84 | 85 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 86 | 87 | ### Xcode Patch ### 88 | *.xcodeproj/* 89 | !*.xcodeproj/project.pbxproj 90 | !*.xcodeproj/xcshareddata/ 91 | !*.xcworkspace/contents.xcworkspacedata 92 | /*.gcno 93 | 94 | # End of https://www.gitignore.io/api/swift,xcode -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: osx 2 | osx_image: xcode10.2 3 | language: swift 4 | script: xcodebuild clean build test -project ViewControllerLifecycleObservers.xcodeproj -scheme "ViewControllerLifecycleObservers" -sdk iphonesimulator -destination "platform=iOS Simulator,OS=11.4,name=iPhone 8" ONLY_ACTIVE_ARCH=YES 5 | after_success: bash <(curl -s https://codecov.io/bash) -J '^ViewControllerLifecycleObservers$' -X xcodellvm -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Essential Developer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UIViewController Lifecycle Observers 2 | 3 | [![Build Status](https://travis-ci.com/essentialdevelopercom/view-controller-lifecycle-observers.svg?branch=master)](https://travis-ci.com/essentialdevelopercom/view-controller-lifecycle-observers) 4 | 5 | Useful UIViewController extension for composing/creating reusable view controllers – no swizzling or subclassing needed! Learn more at: https://www.essentialdeveloper.com/articles/composing-view-controllers-part-3-lifecycle-observers-in-swift 6 | 7 | ``` 8 | controller.onViewWillAppear { 9 | print("viewWillAppear was called!") 10 | } 11 | ``` 12 | 13 | This extension is very useful when composing view controllers with other modules. For example: 14 | 15 | ``` 16 | let analytics = ItemsAnalytics() 17 | itemsListController.onViewDidAppear(run: analytics.reportListPageView) 18 | ``` 19 | 20 | You can also stop receiving messages by removing observers: 21 | 22 | ``` 23 | let service = ItemsService() 24 | let observer = controller.onViewWillAppear(run: service.reloadItems) 25 | observer.remove() 26 | ``` 27 | -------------------------------------------------------------------------------- /ViewControllerLifecycleObservers.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 0814A9C1211883F8009595A0 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0814A9C0211883F8009595A0 /* AppDelegate.swift */; }; 11 | 0814A9C8211883FA009595A0 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0814A9C7211883FA009595A0 /* Assets.xcassets */; }; 12 | 0814A9CB211883FA009595A0 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0814A9C9211883FA009595A0 /* LaunchScreen.storyboard */; }; 13 | 0814A9D0211884C0009595A0 /* ViewControllerLifecycleObservers.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 08E4924D210F2A9D00408C2E /* ViewControllerLifecycleObservers.framework */; }; 14 | 0814A9D1211884C0009595A0 /* ViewControllerLifecycleObservers.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 08E4924D210F2A9D00408C2E /* ViewControllerLifecycleObservers.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 15 | 08E49257210F2A9D00408C2E /* ViewControllerLifecycleObservers.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 08E4924D210F2A9D00408C2E /* ViewControllerLifecycleObservers.framework */; }; 16 | 08E4925C210F2A9D00408C2E /* ViewControllerLifecycleObserversTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08E4925B210F2A9D00408C2E /* ViewControllerLifecycleObserversTests.swift */; }; 17 | 08E49268210F2D0A00408C2E /* UIViewController+LifecycleObservers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08E49267210F2D0A00408C2E /* UIViewController+LifecycleObservers.swift */; }; 18 | /* End PBXBuildFile section */ 19 | 20 | /* Begin PBXContainerItemProxy section */ 21 | 0814A9D2211884C0009595A0 /* PBXContainerItemProxy */ = { 22 | isa = PBXContainerItemProxy; 23 | containerPortal = 08E49244210F2A9D00408C2E /* Project object */; 24 | proxyType = 1; 25 | remoteGlobalIDString = 08E4924C210F2A9D00408C2E; 26 | remoteInfo = ViewControllerLifecycleObservers; 27 | }; 28 | 0814A9D5211889B0009595A0 /* PBXContainerItemProxy */ = { 29 | isa = PBXContainerItemProxy; 30 | containerPortal = 08E49244210F2A9D00408C2E /* Project object */; 31 | proxyType = 1; 32 | remoteGlobalIDString = 0814A9BD211883F8009595A0; 33 | remoteInfo = ViewControllerLifecycleObserversSampleApp; 34 | }; 35 | 08E49258210F2A9D00408C2E /* PBXContainerItemProxy */ = { 36 | isa = PBXContainerItemProxy; 37 | containerPortal = 08E49244210F2A9D00408C2E /* Project object */; 38 | proxyType = 1; 39 | remoteGlobalIDString = 08E4924C210F2A9D00408C2E; 40 | remoteInfo = ViewControllerLifecycleObservers; 41 | }; 42 | /* End PBXContainerItemProxy section */ 43 | 44 | /* Begin PBXCopyFilesBuildPhase section */ 45 | 0814A9D4211884C0009595A0 /* Embed Frameworks */ = { 46 | isa = PBXCopyFilesBuildPhase; 47 | buildActionMask = 2147483647; 48 | dstPath = ""; 49 | dstSubfolderSpec = 10; 50 | files = ( 51 | 0814A9D1211884C0009595A0 /* ViewControllerLifecycleObservers.framework in Embed Frameworks */, 52 | ); 53 | name = "Embed Frameworks"; 54 | runOnlyForDeploymentPostprocessing = 0; 55 | }; 56 | /* End PBXCopyFilesBuildPhase section */ 57 | 58 | /* Begin PBXFileReference section */ 59 | 0814A9BE211883F8009595A0 /* ViewControllerLifecycleObserversSampleApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ViewControllerLifecycleObserversSampleApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; 60 | 0814A9C0211883F8009595A0 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 61 | 0814A9C7211883FA009595A0 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 62 | 0814A9CA211883FA009595A0 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 63 | 0814A9CC211883FA009595A0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 64 | 08E4924D210F2A9D00408C2E /* ViewControllerLifecycleObservers.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = ViewControllerLifecycleObservers.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 65 | 08E49251210F2A9D00408C2E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 66 | 08E49256210F2A9D00408C2E /* ViewControllerLifecycleObserversTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ViewControllerLifecycleObserversTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 67 | 08E4925B210F2A9D00408C2E /* ViewControllerLifecycleObserversTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewControllerLifecycleObserversTests.swift; sourceTree = ""; }; 68 | 08E4925D210F2A9D00408C2E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 69 | 08E49267210F2D0A00408C2E /* UIViewController+LifecycleObservers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+LifecycleObservers.swift"; sourceTree = ""; }; 70 | /* End PBXFileReference section */ 71 | 72 | /* Begin PBXFrameworksBuildPhase section */ 73 | 0814A9BB211883F8009595A0 /* Frameworks */ = { 74 | isa = PBXFrameworksBuildPhase; 75 | buildActionMask = 2147483647; 76 | files = ( 77 | 0814A9D0211884C0009595A0 /* ViewControllerLifecycleObservers.framework in Frameworks */, 78 | ); 79 | runOnlyForDeploymentPostprocessing = 0; 80 | }; 81 | 08E49249210F2A9D00408C2E /* Frameworks */ = { 82 | isa = PBXFrameworksBuildPhase; 83 | buildActionMask = 2147483647; 84 | files = ( 85 | ); 86 | runOnlyForDeploymentPostprocessing = 0; 87 | }; 88 | 08E49253210F2A9D00408C2E /* Frameworks */ = { 89 | isa = PBXFrameworksBuildPhase; 90 | buildActionMask = 2147483647; 91 | files = ( 92 | 08E49257210F2A9D00408C2E /* ViewControllerLifecycleObservers.framework in Frameworks */, 93 | ); 94 | runOnlyForDeploymentPostprocessing = 0; 95 | }; 96 | /* End PBXFrameworksBuildPhase section */ 97 | 98 | /* Begin PBXGroup section */ 99 | 0814A9BF211883F8009595A0 /* ViewControllerLifecycleObserversSampleApp */ = { 100 | isa = PBXGroup; 101 | children = ( 102 | 0814A9C0211883F8009595A0 /* AppDelegate.swift */, 103 | 0814A9D721188C14009595A0 /* Supporting Files */, 104 | ); 105 | path = ViewControllerLifecycleObserversSampleApp; 106 | sourceTree = ""; 107 | }; 108 | 0814A9D721188C14009595A0 /* Supporting Files */ = { 109 | isa = PBXGroup; 110 | children = ( 111 | 0814A9C7211883FA009595A0 /* Assets.xcassets */, 112 | 0814A9C9211883FA009595A0 /* LaunchScreen.storyboard */, 113 | 0814A9CC211883FA009595A0 /* Info.plist */, 114 | ); 115 | name = "Supporting Files"; 116 | sourceTree = ""; 117 | }; 118 | 08E49243210F2A9D00408C2E = { 119 | isa = PBXGroup; 120 | children = ( 121 | 08E4924F210F2A9D00408C2E /* ViewControllerLifecycleObservers */, 122 | 08E4925A210F2A9D00408C2E /* ViewControllerLifecycleObserversTests */, 123 | 0814A9BF211883F8009595A0 /* ViewControllerLifecycleObserversSampleApp */, 124 | 08E4924E210F2A9D00408C2E /* Products */, 125 | ); 126 | sourceTree = ""; 127 | }; 128 | 08E4924E210F2A9D00408C2E /* Products */ = { 129 | isa = PBXGroup; 130 | children = ( 131 | 08E4924D210F2A9D00408C2E /* ViewControllerLifecycleObservers.framework */, 132 | 08E49256210F2A9D00408C2E /* ViewControllerLifecycleObserversTests.xctest */, 133 | 0814A9BE211883F8009595A0 /* ViewControllerLifecycleObserversSampleApp.app */, 134 | ); 135 | name = Products; 136 | sourceTree = ""; 137 | }; 138 | 08E4924F210F2A9D00408C2E /* ViewControllerLifecycleObservers */ = { 139 | isa = PBXGroup; 140 | children = ( 141 | 08E49267210F2D0A00408C2E /* UIViewController+LifecycleObservers.swift */, 142 | 08E49251210F2A9D00408C2E /* Info.plist */, 143 | ); 144 | path = ViewControllerLifecycleObservers; 145 | sourceTree = ""; 146 | }; 147 | 08E4925A210F2A9D00408C2E /* ViewControllerLifecycleObserversTests */ = { 148 | isa = PBXGroup; 149 | children = ( 150 | 08E4925B210F2A9D00408C2E /* ViewControllerLifecycleObserversTests.swift */, 151 | 08E4925D210F2A9D00408C2E /* Info.plist */, 152 | ); 153 | path = ViewControllerLifecycleObserversTests; 154 | sourceTree = ""; 155 | }; 156 | /* End PBXGroup section */ 157 | 158 | /* Begin PBXHeadersBuildPhase section */ 159 | 08E4924A210F2A9D00408C2E /* Headers */ = { 160 | isa = PBXHeadersBuildPhase; 161 | buildActionMask = 2147483647; 162 | files = ( 163 | ); 164 | runOnlyForDeploymentPostprocessing = 0; 165 | }; 166 | /* End PBXHeadersBuildPhase section */ 167 | 168 | /* Begin PBXNativeTarget section */ 169 | 0814A9BD211883F8009595A0 /* ViewControllerLifecycleObserversSampleApp */ = { 170 | isa = PBXNativeTarget; 171 | buildConfigurationList = 0814A9CF211883FA009595A0 /* Build configuration list for PBXNativeTarget "ViewControllerLifecycleObserversSampleApp" */; 172 | buildPhases = ( 173 | 0814A9BA211883F8009595A0 /* Sources */, 174 | 0814A9BB211883F8009595A0 /* Frameworks */, 175 | 0814A9BC211883F8009595A0 /* Resources */, 176 | 0814A9D4211884C0009595A0 /* Embed Frameworks */, 177 | ); 178 | buildRules = ( 179 | ); 180 | dependencies = ( 181 | 0814A9D3211884C0009595A0 /* PBXTargetDependency */, 182 | ); 183 | name = ViewControllerLifecycleObserversSampleApp; 184 | productName = ViewControllerLifecycleObserversSampleApp; 185 | productReference = 0814A9BE211883F8009595A0 /* ViewControllerLifecycleObserversSampleApp.app */; 186 | productType = "com.apple.product-type.application"; 187 | }; 188 | 08E4924C210F2A9D00408C2E /* ViewControllerLifecycleObservers */ = { 189 | isa = PBXNativeTarget; 190 | buildConfigurationList = 08E49261210F2A9D00408C2E /* Build configuration list for PBXNativeTarget "ViewControllerLifecycleObservers" */; 191 | buildPhases = ( 192 | 08E49248210F2A9D00408C2E /* Sources */, 193 | 08E49249210F2A9D00408C2E /* Frameworks */, 194 | 08E4924A210F2A9D00408C2E /* Headers */, 195 | 08E4924B210F2A9D00408C2E /* Resources */, 196 | ); 197 | buildRules = ( 198 | ); 199 | dependencies = ( 200 | ); 201 | name = ViewControllerLifecycleObservers; 202 | productName = ViewControllerLifecycleObservers; 203 | productReference = 08E4924D210F2A9D00408C2E /* ViewControllerLifecycleObservers.framework */; 204 | productType = "com.apple.product-type.framework"; 205 | }; 206 | 08E49255210F2A9D00408C2E /* ViewControllerLifecycleObserversTests */ = { 207 | isa = PBXNativeTarget; 208 | buildConfigurationList = 08E49264210F2A9D00408C2E /* Build configuration list for PBXNativeTarget "ViewControllerLifecycleObserversTests" */; 209 | buildPhases = ( 210 | 08E49252210F2A9D00408C2E /* Sources */, 211 | 08E49253210F2A9D00408C2E /* Frameworks */, 212 | 08E49254210F2A9D00408C2E /* Resources */, 213 | ); 214 | buildRules = ( 215 | ); 216 | dependencies = ( 217 | 08E49259210F2A9D00408C2E /* PBXTargetDependency */, 218 | 0814A9D6211889B0009595A0 /* PBXTargetDependency */, 219 | ); 220 | name = ViewControllerLifecycleObserversTests; 221 | productName = ViewControllerLifecycleObserversTests; 222 | productReference = 08E49256210F2A9D00408C2E /* ViewControllerLifecycleObserversTests.xctest */; 223 | productType = "com.apple.product-type.bundle.unit-test"; 224 | }; 225 | /* End PBXNativeTarget section */ 226 | 227 | /* Begin PBXProject section */ 228 | 08E49244210F2A9D00408C2E /* Project object */ = { 229 | isa = PBXProject; 230 | attributes = { 231 | LastSwiftUpdateCheck = 0940; 232 | LastUpgradeCheck = 0940; 233 | ORGANIZATIONNAME = "Essential Developer"; 234 | TargetAttributes = { 235 | 0814A9BD211883F8009595A0 = { 236 | CreatedOnToolsVersion = 9.4.1; 237 | LastSwiftMigration = 1020; 238 | }; 239 | 08E4924C210F2A9D00408C2E = { 240 | CreatedOnToolsVersion = 9.4.1; 241 | LastSwiftMigration = 1020; 242 | }; 243 | 08E49255210F2A9D00408C2E = { 244 | CreatedOnToolsVersion = 9.4.1; 245 | LastSwiftMigration = 1020; 246 | TestTargetID = 0814A9BD211883F8009595A0; 247 | }; 248 | }; 249 | }; 250 | buildConfigurationList = 08E49247210F2A9D00408C2E /* Build configuration list for PBXProject "ViewControllerLifecycleObservers" */; 251 | compatibilityVersion = "Xcode 9.3"; 252 | developmentRegion = en; 253 | hasScannedForEncodings = 0; 254 | knownRegions = ( 255 | en, 256 | Base, 257 | ); 258 | mainGroup = 08E49243210F2A9D00408C2E; 259 | productRefGroup = 08E4924E210F2A9D00408C2E /* Products */; 260 | projectDirPath = ""; 261 | projectRoot = ""; 262 | targets = ( 263 | 08E4924C210F2A9D00408C2E /* ViewControllerLifecycleObservers */, 264 | 08E49255210F2A9D00408C2E /* ViewControllerLifecycleObserversTests */, 265 | 0814A9BD211883F8009595A0 /* ViewControllerLifecycleObserversSampleApp */, 266 | ); 267 | }; 268 | /* End PBXProject section */ 269 | 270 | /* Begin PBXResourcesBuildPhase section */ 271 | 0814A9BC211883F8009595A0 /* Resources */ = { 272 | isa = PBXResourcesBuildPhase; 273 | buildActionMask = 2147483647; 274 | files = ( 275 | 0814A9CB211883FA009595A0 /* LaunchScreen.storyboard in Resources */, 276 | 0814A9C8211883FA009595A0 /* Assets.xcassets in Resources */, 277 | ); 278 | runOnlyForDeploymentPostprocessing = 0; 279 | }; 280 | 08E4924B210F2A9D00408C2E /* Resources */ = { 281 | isa = PBXResourcesBuildPhase; 282 | buildActionMask = 2147483647; 283 | files = ( 284 | ); 285 | runOnlyForDeploymentPostprocessing = 0; 286 | }; 287 | 08E49254210F2A9D00408C2E /* Resources */ = { 288 | isa = PBXResourcesBuildPhase; 289 | buildActionMask = 2147483647; 290 | files = ( 291 | ); 292 | runOnlyForDeploymentPostprocessing = 0; 293 | }; 294 | /* End PBXResourcesBuildPhase section */ 295 | 296 | /* Begin PBXSourcesBuildPhase section */ 297 | 0814A9BA211883F8009595A0 /* Sources */ = { 298 | isa = PBXSourcesBuildPhase; 299 | buildActionMask = 2147483647; 300 | files = ( 301 | 0814A9C1211883F8009595A0 /* AppDelegate.swift in Sources */, 302 | ); 303 | runOnlyForDeploymentPostprocessing = 0; 304 | }; 305 | 08E49248210F2A9D00408C2E /* Sources */ = { 306 | isa = PBXSourcesBuildPhase; 307 | buildActionMask = 2147483647; 308 | files = ( 309 | 08E49268210F2D0A00408C2E /* UIViewController+LifecycleObservers.swift in Sources */, 310 | ); 311 | runOnlyForDeploymentPostprocessing = 0; 312 | }; 313 | 08E49252210F2A9D00408C2E /* Sources */ = { 314 | isa = PBXSourcesBuildPhase; 315 | buildActionMask = 2147483647; 316 | files = ( 317 | 08E4925C210F2A9D00408C2E /* ViewControllerLifecycleObserversTests.swift in Sources */, 318 | ); 319 | runOnlyForDeploymentPostprocessing = 0; 320 | }; 321 | /* End PBXSourcesBuildPhase section */ 322 | 323 | /* Begin PBXTargetDependency section */ 324 | 0814A9D3211884C0009595A0 /* PBXTargetDependency */ = { 325 | isa = PBXTargetDependency; 326 | target = 08E4924C210F2A9D00408C2E /* ViewControllerLifecycleObservers */; 327 | targetProxy = 0814A9D2211884C0009595A0 /* PBXContainerItemProxy */; 328 | }; 329 | 0814A9D6211889B0009595A0 /* PBXTargetDependency */ = { 330 | isa = PBXTargetDependency; 331 | target = 0814A9BD211883F8009595A0 /* ViewControllerLifecycleObserversSampleApp */; 332 | targetProxy = 0814A9D5211889B0009595A0 /* PBXContainerItemProxy */; 333 | }; 334 | 08E49259210F2A9D00408C2E /* PBXTargetDependency */ = { 335 | isa = PBXTargetDependency; 336 | target = 08E4924C210F2A9D00408C2E /* ViewControllerLifecycleObservers */; 337 | targetProxy = 08E49258210F2A9D00408C2E /* PBXContainerItemProxy */; 338 | }; 339 | /* End PBXTargetDependency section */ 340 | 341 | /* Begin PBXVariantGroup section */ 342 | 0814A9C9211883FA009595A0 /* LaunchScreen.storyboard */ = { 343 | isa = PBXVariantGroup; 344 | children = ( 345 | 0814A9CA211883FA009595A0 /* Base */, 346 | ); 347 | name = LaunchScreen.storyboard; 348 | sourceTree = ""; 349 | }; 350 | /* End PBXVariantGroup section */ 351 | 352 | /* Begin XCBuildConfiguration section */ 353 | 0814A9CD211883FA009595A0 /* Debug */ = { 354 | isa = XCBuildConfiguration; 355 | buildSettings = { 356 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 357 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 358 | CODE_SIGN_STYLE = Automatic; 359 | DEVELOPMENT_TEAM = VRJ2W4578X; 360 | INFOPLIST_FILE = ViewControllerLifecycleObserversSampleApp/Info.plist; 361 | IPHONEOS_DEPLOYMENT_TARGET = 11.4; 362 | LD_RUNPATH_SEARCH_PATHS = ( 363 | "$(inherited)", 364 | "@executable_path/Frameworks", 365 | ); 366 | PRODUCT_BUNDLE_IDENTIFIER = com.essentialdeveloper.ViewControllerLifecycleObserversSampleApp; 367 | PRODUCT_NAME = "$(TARGET_NAME)"; 368 | SWIFT_VERSION = 5.0; 369 | TARGETED_DEVICE_FAMILY = "1,2"; 370 | }; 371 | name = Debug; 372 | }; 373 | 0814A9CE211883FA009595A0 /* Release */ = { 374 | isa = XCBuildConfiguration; 375 | buildSettings = { 376 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 377 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 378 | CODE_SIGN_STYLE = Automatic; 379 | DEVELOPMENT_TEAM = VRJ2W4578X; 380 | INFOPLIST_FILE = ViewControllerLifecycleObserversSampleApp/Info.plist; 381 | IPHONEOS_DEPLOYMENT_TARGET = 11.4; 382 | LD_RUNPATH_SEARCH_PATHS = ( 383 | "$(inherited)", 384 | "@executable_path/Frameworks", 385 | ); 386 | PRODUCT_BUNDLE_IDENTIFIER = com.essentialdeveloper.ViewControllerLifecycleObserversSampleApp; 387 | PRODUCT_NAME = "$(TARGET_NAME)"; 388 | SWIFT_VERSION = 5.0; 389 | TARGETED_DEVICE_FAMILY = "1,2"; 390 | }; 391 | name = Release; 392 | }; 393 | 08E4925F210F2A9D00408C2E /* Debug */ = { 394 | isa = XCBuildConfiguration; 395 | buildSettings = { 396 | ALWAYS_SEARCH_USER_PATHS = NO; 397 | CLANG_ANALYZER_NONNULL = YES; 398 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 399 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 400 | CLANG_CXX_LIBRARY = "libc++"; 401 | CLANG_ENABLE_MODULES = YES; 402 | CLANG_ENABLE_OBJC_ARC = YES; 403 | CLANG_ENABLE_OBJC_WEAK = YES; 404 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 405 | CLANG_WARN_BOOL_CONVERSION = YES; 406 | CLANG_WARN_COMMA = YES; 407 | CLANG_WARN_CONSTANT_CONVERSION = YES; 408 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 409 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 410 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 411 | CLANG_WARN_EMPTY_BODY = YES; 412 | CLANG_WARN_ENUM_CONVERSION = YES; 413 | CLANG_WARN_INFINITE_RECURSION = YES; 414 | CLANG_WARN_INT_CONVERSION = YES; 415 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 416 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 417 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 418 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 419 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 420 | CLANG_WARN_STRICT_PROTOTYPES = YES; 421 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 422 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 423 | CLANG_WARN_UNREACHABLE_CODE = YES; 424 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 425 | CODE_SIGN_IDENTITY = "iPhone Developer"; 426 | COPY_PHASE_STRIP = NO; 427 | CURRENT_PROJECT_VERSION = 1; 428 | DEBUG_INFORMATION_FORMAT = dwarf; 429 | ENABLE_STRICT_OBJC_MSGSEND = YES; 430 | ENABLE_TESTABILITY = YES; 431 | GCC_C_LANGUAGE_STANDARD = gnu11; 432 | GCC_DYNAMIC_NO_PIC = NO; 433 | GCC_NO_COMMON_BLOCKS = YES; 434 | GCC_OPTIMIZATION_LEVEL = 0; 435 | GCC_PREPROCESSOR_DEFINITIONS = ( 436 | "DEBUG=1", 437 | "$(inherited)", 438 | ); 439 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 440 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 441 | GCC_WARN_UNDECLARED_SELECTOR = YES; 442 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 443 | GCC_WARN_UNUSED_FUNCTION = YES; 444 | GCC_WARN_UNUSED_VARIABLE = YES; 445 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 446 | MTL_ENABLE_DEBUG_INFO = YES; 447 | ONLY_ACTIVE_ARCH = YES; 448 | SDKROOT = iphoneos; 449 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 450 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 451 | VERSIONING_SYSTEM = "apple-generic"; 452 | VERSION_INFO_PREFIX = ""; 453 | }; 454 | name = Debug; 455 | }; 456 | 08E49260210F2A9D00408C2E /* Release */ = { 457 | isa = XCBuildConfiguration; 458 | buildSettings = { 459 | ALWAYS_SEARCH_USER_PATHS = NO; 460 | CLANG_ANALYZER_NONNULL = YES; 461 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 462 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 463 | CLANG_CXX_LIBRARY = "libc++"; 464 | CLANG_ENABLE_MODULES = YES; 465 | CLANG_ENABLE_OBJC_ARC = YES; 466 | CLANG_ENABLE_OBJC_WEAK = YES; 467 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 468 | CLANG_WARN_BOOL_CONVERSION = YES; 469 | CLANG_WARN_COMMA = YES; 470 | CLANG_WARN_CONSTANT_CONVERSION = YES; 471 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 472 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 473 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 474 | CLANG_WARN_EMPTY_BODY = YES; 475 | CLANG_WARN_ENUM_CONVERSION = YES; 476 | CLANG_WARN_INFINITE_RECURSION = YES; 477 | CLANG_WARN_INT_CONVERSION = YES; 478 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 479 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 480 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 481 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 482 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 483 | CLANG_WARN_STRICT_PROTOTYPES = YES; 484 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 485 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 486 | CLANG_WARN_UNREACHABLE_CODE = YES; 487 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 488 | CODE_SIGN_IDENTITY = "iPhone Developer"; 489 | COPY_PHASE_STRIP = NO; 490 | CURRENT_PROJECT_VERSION = 1; 491 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 492 | ENABLE_NS_ASSERTIONS = NO; 493 | ENABLE_STRICT_OBJC_MSGSEND = YES; 494 | GCC_C_LANGUAGE_STANDARD = gnu11; 495 | GCC_NO_COMMON_BLOCKS = YES; 496 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 497 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 498 | GCC_WARN_UNDECLARED_SELECTOR = YES; 499 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 500 | GCC_WARN_UNUSED_FUNCTION = YES; 501 | GCC_WARN_UNUSED_VARIABLE = YES; 502 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 503 | MTL_ENABLE_DEBUG_INFO = NO; 504 | SDKROOT = iphoneos; 505 | SWIFT_COMPILATION_MODE = wholemodule; 506 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 507 | VALIDATE_PRODUCT = YES; 508 | VERSIONING_SYSTEM = "apple-generic"; 509 | VERSION_INFO_PREFIX = ""; 510 | }; 511 | name = Release; 512 | }; 513 | 08E49262210F2A9D00408C2E /* Debug */ = { 514 | isa = XCBuildConfiguration; 515 | buildSettings = { 516 | CLANG_ENABLE_MODULES = YES; 517 | CODE_SIGN_IDENTITY = ""; 518 | CODE_SIGN_STYLE = Automatic; 519 | DEFINES_MODULE = YES; 520 | DEVELOPMENT_TEAM = VRJ2W4578X; 521 | DYLIB_COMPATIBILITY_VERSION = 1; 522 | DYLIB_CURRENT_VERSION = 1; 523 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 524 | INFOPLIST_FILE = ViewControllerLifecycleObservers/Info.plist; 525 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 526 | LD_RUNPATH_SEARCH_PATHS = ( 527 | "$(inherited)", 528 | "@executable_path/Frameworks", 529 | "@loader_path/Frameworks", 530 | ); 531 | PRODUCT_BUNDLE_IDENTIFIER = com.essentialdeveloper.ViewControllerLifecycleObservers; 532 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 533 | SKIP_INSTALL = YES; 534 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 535 | SWIFT_VERSION = 5.0; 536 | TARGETED_DEVICE_FAMILY = "1,2"; 537 | }; 538 | name = Debug; 539 | }; 540 | 08E49263210F2A9D00408C2E /* Release */ = { 541 | isa = XCBuildConfiguration; 542 | buildSettings = { 543 | CLANG_ENABLE_MODULES = YES; 544 | CODE_SIGN_IDENTITY = ""; 545 | CODE_SIGN_STYLE = Automatic; 546 | DEFINES_MODULE = YES; 547 | DEVELOPMENT_TEAM = VRJ2W4578X; 548 | DYLIB_COMPATIBILITY_VERSION = 1; 549 | DYLIB_CURRENT_VERSION = 1; 550 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 551 | INFOPLIST_FILE = ViewControllerLifecycleObservers/Info.plist; 552 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 553 | LD_RUNPATH_SEARCH_PATHS = ( 554 | "$(inherited)", 555 | "@executable_path/Frameworks", 556 | "@loader_path/Frameworks", 557 | ); 558 | PRODUCT_BUNDLE_IDENTIFIER = com.essentialdeveloper.ViewControllerLifecycleObservers; 559 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 560 | SKIP_INSTALL = YES; 561 | SWIFT_VERSION = 5.0; 562 | TARGETED_DEVICE_FAMILY = "1,2"; 563 | }; 564 | name = Release; 565 | }; 566 | 08E49265210F2A9D00408C2E /* Debug */ = { 567 | isa = XCBuildConfiguration; 568 | buildSettings = { 569 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 570 | CODE_SIGN_STYLE = Automatic; 571 | DEVELOPMENT_TEAM = VRJ2W4578X; 572 | INFOPLIST_FILE = ViewControllerLifecycleObserversTests/Info.plist; 573 | LD_RUNPATH_SEARCH_PATHS = ( 574 | "$(inherited)", 575 | "@executable_path/Frameworks", 576 | "@loader_path/Frameworks", 577 | ); 578 | PRODUCT_BUNDLE_IDENTIFIER = com.essentialdeveloper.ViewControllerLifecycleObserversTests; 579 | PRODUCT_NAME = "$(TARGET_NAME)"; 580 | SWIFT_VERSION = 5.0; 581 | TARGETED_DEVICE_FAMILY = "1,2"; 582 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ViewControllerLifecycleObserversSampleApp.app/ViewControllerLifecycleObserversSampleApp"; 583 | }; 584 | name = Debug; 585 | }; 586 | 08E49266210F2A9D00408C2E /* Release */ = { 587 | isa = XCBuildConfiguration; 588 | buildSettings = { 589 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 590 | CODE_SIGN_STYLE = Automatic; 591 | DEVELOPMENT_TEAM = VRJ2W4578X; 592 | INFOPLIST_FILE = ViewControllerLifecycleObserversTests/Info.plist; 593 | LD_RUNPATH_SEARCH_PATHS = ( 594 | "$(inherited)", 595 | "@executable_path/Frameworks", 596 | "@loader_path/Frameworks", 597 | ); 598 | PRODUCT_BUNDLE_IDENTIFIER = com.essentialdeveloper.ViewControllerLifecycleObserversTests; 599 | PRODUCT_NAME = "$(TARGET_NAME)"; 600 | SWIFT_VERSION = 5.0; 601 | TARGETED_DEVICE_FAMILY = "1,2"; 602 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ViewControllerLifecycleObserversSampleApp.app/ViewControllerLifecycleObserversSampleApp"; 603 | }; 604 | name = Release; 605 | }; 606 | /* End XCBuildConfiguration section */ 607 | 608 | /* Begin XCConfigurationList section */ 609 | 0814A9CF211883FA009595A0 /* Build configuration list for PBXNativeTarget "ViewControllerLifecycleObserversSampleApp" */ = { 610 | isa = XCConfigurationList; 611 | buildConfigurations = ( 612 | 0814A9CD211883FA009595A0 /* Debug */, 613 | 0814A9CE211883FA009595A0 /* Release */, 614 | ); 615 | defaultConfigurationIsVisible = 0; 616 | defaultConfigurationName = Release; 617 | }; 618 | 08E49247210F2A9D00408C2E /* Build configuration list for PBXProject "ViewControllerLifecycleObservers" */ = { 619 | isa = XCConfigurationList; 620 | buildConfigurations = ( 621 | 08E4925F210F2A9D00408C2E /* Debug */, 622 | 08E49260210F2A9D00408C2E /* Release */, 623 | ); 624 | defaultConfigurationIsVisible = 0; 625 | defaultConfigurationName = Release; 626 | }; 627 | 08E49261210F2A9D00408C2E /* Build configuration list for PBXNativeTarget "ViewControllerLifecycleObservers" */ = { 628 | isa = XCConfigurationList; 629 | buildConfigurations = ( 630 | 08E49262210F2A9D00408C2E /* Debug */, 631 | 08E49263210F2A9D00408C2E /* Release */, 632 | ); 633 | defaultConfigurationIsVisible = 0; 634 | defaultConfigurationName = Release; 635 | }; 636 | 08E49264210F2A9D00408C2E /* Build configuration list for PBXNativeTarget "ViewControllerLifecycleObserversTests" */ = { 637 | isa = XCConfigurationList; 638 | buildConfigurations = ( 639 | 08E49265210F2A9D00408C2E /* Debug */, 640 | 08E49266210F2A9D00408C2E /* Release */, 641 | ); 642 | defaultConfigurationIsVisible = 0; 643 | defaultConfigurationName = Release; 644 | }; 645 | /* End XCConfigurationList section */ 646 | }; 647 | rootObject = 08E49244210F2A9D00408C2E /* Project object */; 648 | } 649 | -------------------------------------------------------------------------------- /ViewControllerLifecycleObservers.xcodeproj/xcshareddata/xcschemes/ViewControllerLifecycleObservers.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 34 | 40 | 41 | 42 | 43 | 44 | 50 | 51 | 52 | 53 | 54 | 55 | 65 | 66 | 72 | 73 | 74 | 75 | 76 | 77 | 83 | 84 | 90 | 91 | 92 | 93 | 95 | 96 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /ViewControllerLifecycleObservers.xcodeproj/xcshareddata/xcschemes/ViewControllerLifecycleObserversSampleApp.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 34 | 40 | 41 | 42 | 43 | 44 | 50 | 51 | 52 | 53 | 54 | 55 | 65 | 67 | 73 | 74 | 75 | 76 | 77 | 78 | 84 | 86 | 92 | 93 | 94 | 95 | 97 | 98 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /ViewControllerLifecycleObservers/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSPrincipalClass 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /ViewControllerLifecycleObservers/UIViewController+LifecycleObservers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2018 Essential Developer. All rights reserved. 3 | // 4 | 5 | import UIKit 6 | 7 | public protocol Observer { 8 | func remove() 9 | } 10 | 11 | public extension UIViewController { 12 | 13 | /// Event that notifies the view controller is about to be added to a view hierarchy. 14 | /// 15 | /// - Parameter callback: The closure to execute. 16 | /// - Returns: The observer for unsubscribing to event. 17 | @discardableResult 18 | func onViewWillAppear(run callback: @escaping () -> Void) -> Observer { 19 | return ViewControllerLifecycleObserver( 20 | parent: self, 21 | viewWillAppearCallback: callback 22 | ) 23 | } 24 | 25 | /// Event that notifies the view controller was added to a view hierarchy. 26 | /// 27 | /// - Parameter callback: The closure to execute. 28 | /// - Returns: The observer for unsubscribing to event. 29 | @discardableResult 30 | func onViewDidAppear(run callback: @escaping () -> Void) -> Observer { 31 | return ViewControllerLifecycleObserver( 32 | parent: self, 33 | viewDidAppearCallback: callback 34 | ) 35 | } 36 | 37 | /// Event that notifies the view controller is about to be removed from a view hierarchy. 38 | /// 39 | /// - Parameter callback: The closure to execute. 40 | /// - Returns: The observer for unsubscribing to event. 41 | @discardableResult 42 | func onViewWillDisappear(run callback: @escaping () -> Void) -> Observer { 43 | return ViewControllerLifecycleObserver( 44 | parent: self, 45 | viewWillDisappearCallback: callback 46 | ) 47 | } 48 | 49 | /// Event that notifies the view controller was removed from a view hierarchy. 50 | /// 51 | /// - Parameter callback: The closure to execute. 52 | /// - Returns: The observer for unsubscribing to event. 53 | @discardableResult 54 | func onViewDidDisappear(run callback: @escaping () -> Void) -> Observer { 55 | return ViewControllerLifecycleObserver( 56 | parent: self, 57 | viewDidDisappearCallback: callback 58 | ) 59 | } 60 | } 61 | 62 | private class ViewControllerLifecycleObserver: UIViewController, Observer { 63 | private var viewWillAppearCallback: (() -> Void)? = nil 64 | private var viewDidAppearCallback: (() -> Void)? = nil 65 | 66 | private var viewWillDisappearCallback: (() -> Void)? = nil 67 | private var viewDidDisappearCallback: (() -> Void)? = nil 68 | 69 | convenience init( 70 | parent: UIViewController, 71 | viewWillAppearCallback: (() -> Void)? = nil, 72 | viewDidAppearCallback: (() -> Void)? = nil, 73 | viewWillDisappearCallback: (() -> Void)? = nil, 74 | viewDidDisappearCallback: (() -> Void)? = nil) { 75 | self.init() 76 | self.add(to: parent) 77 | self.viewWillAppearCallback = viewWillAppearCallback 78 | self.viewDidAppearCallback = viewDidAppearCallback 79 | self.viewWillDisappearCallback = viewWillDisappearCallback 80 | self.viewDidDisappearCallback = viewDidDisappearCallback 81 | } 82 | 83 | override func viewWillAppear(_ animated: Bool) { 84 | super.viewWillAppear(animated) 85 | 86 | viewWillAppearCallback?() 87 | } 88 | 89 | override func viewDidAppear(_ animated: Bool) { 90 | super.viewDidAppear(animated) 91 | 92 | viewDidAppearCallback?() 93 | } 94 | 95 | override func viewWillDisappear(_ animated: Bool) { 96 | super.viewWillDisappear(animated) 97 | 98 | viewWillDisappearCallback?() 99 | } 100 | 101 | override func viewDidDisappear(_ animated: Bool) { 102 | super.viewDidDisappear(animated) 103 | 104 | viewDidDisappearCallback?() 105 | } 106 | 107 | private func add(to parent: UIViewController) { 108 | parent.addChild(self) 109 | view.isHidden = true 110 | parent.view.addSubview(view) 111 | didMove(toParent: parent) 112 | } 113 | 114 | func remove() { 115 | willMove(toParent: nil) 116 | view.removeFromSuperview() 117 | removeFromParent() 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /ViewControllerLifecycleObserversSampleApp/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2018 Essential Developer. All rights reserved. 3 | // 4 | 5 | import UIKit 6 | import ViewControllerLifecycleObservers 7 | 8 | @UIApplicationMain 9 | class AppDelegate: UIResponder, UIApplicationDelegate { 10 | var window: UIWindow? 11 | 12 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 13 | return true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /ViewControllerLifecycleObserversSampleApp/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /ViewControllerLifecycleObserversSampleApp/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /ViewControllerLifecycleObserversSampleApp/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /ViewControllerLifecycleObserversSampleApp/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIRequiredDeviceCapabilities 26 | 27 | armv7 28 | 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /ViewControllerLifecycleObserversTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /ViewControllerLifecycleObserversTests/ViewControllerLifecycleObserversTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2018 Essential Developer. All rights reserved. 3 | // 4 | 5 | import XCTest 6 | import ViewControllerLifecycleObservers 7 | 8 | class ViewControllerLifecycleObserversTests: XCTestCase { 9 | 10 | // MARK: View Will Appear Tests 11 | 12 | func testViewWillAppearObserverIsAddedAsChild() { 13 | assertObserverIsAddedAsChild(when: { sut in 14 | sut.onViewWillAppear {} 15 | }) 16 | } 17 | 18 | func testViewWillAppearObserverViewIsAddedAsSubview() { 19 | assertObserverViewIsAddedAsSubview(when: { sut in 20 | sut.onViewWillAppear {} 21 | }) 22 | } 23 | 24 | func testViewWillAppearObserverViewIsInvisible() { 25 | assertObserverViewIsInvisible(when: { sut in 26 | sut.onViewWillAppear {} 27 | }) 28 | } 29 | 30 | func testViewWillAppearObserverFiresCallback() { 31 | assertObserver( 32 | firesCallback: { $0.onViewWillAppear }, 33 | when: { $0.viewWillAppear(false) }) 34 | } 35 | 36 | func testCanRemoveViewWillAppearObserver() { 37 | assertCanRemoveObserver(when: { sut in 38 | sut.onViewWillAppear {} 39 | }) 40 | } 41 | 42 | func testCanRemoveViewWillAppearObserverView() { 43 | assertCanRemoveObserverView(when: { sut in 44 | sut.onViewWillAppear {} 45 | }) 46 | } 47 | 48 | // MARK: View Did Appear Tests 49 | 50 | func testViewDidAppearObserverIsAddedAsChild() { 51 | assertObserverIsAddedAsChild(when: { sut in 52 | sut.onViewDidAppear {} 53 | }) 54 | } 55 | 56 | func testViewDidAppearObserverViewIsAddedAsSubview() { 57 | assertObserverViewIsAddedAsSubview(when: { sut in 58 | sut.onViewDidAppear {} 59 | }) 60 | } 61 | 62 | func testViewDidAppearObserverViewIsInvisible() { 63 | assertObserverViewIsInvisible(when: { sut in 64 | sut.onViewDidAppear {} 65 | }) 66 | } 67 | 68 | func testViewDidAppearObserverFiresCallback() { 69 | assertObserver( 70 | firesCallback: { $0.onViewDidAppear }, 71 | when: { $0.viewDidAppear(false) }) 72 | } 73 | 74 | func testCanRemoveViewDidAppearObserver() { 75 | assertCanRemoveObserver(when: { sut in 76 | sut.onViewDidAppear {} 77 | }) 78 | } 79 | 80 | func testCanRemoveViewDidAppearObserverView() { 81 | assertCanRemoveObserverView(when: { sut in 82 | sut.onViewDidAppear {} 83 | }) 84 | } 85 | 86 | // MARK: View Will Disappear Tests 87 | 88 | func testViewWillDisappearObserverIsAddedAsChild() { 89 | assertObserverIsAddedAsChild(when: { sut in 90 | sut.onViewWillDisappear {} 91 | }) 92 | } 93 | 94 | func testViewWillDisappearObserverViewIsAddedAsSubview() { 95 | assertObserverViewIsAddedAsSubview(when: { sut in 96 | sut.onViewWillDisappear {} 97 | }) 98 | } 99 | 100 | func testViewWillDisappearObserverViewIsInvisible() { 101 | assertObserverViewIsInvisible(when: { sut in 102 | sut.onViewWillDisappear {} 103 | }) 104 | } 105 | 106 | func testViewWillDisappearObserverFiresCallback() { 107 | assertObserver( 108 | firesCallback: { $0.onViewWillDisappear }, 109 | when: { $0.viewWillDisappear(false) }) 110 | } 111 | 112 | func testCanRemoveViewWillDisappearObserver() { 113 | assertCanRemoveObserver(when: { sut in 114 | sut.onViewWillDisappear {} 115 | }) 116 | } 117 | 118 | func testCanRemoveViewWillDisappearObserverView() { 119 | assertCanRemoveObserverView(when: { sut in 120 | sut.onViewWillDisappear {} 121 | }) 122 | } 123 | 124 | // MARK: View Did Disappear Tests 125 | 126 | func testViewDidDisappearObserverIsAddedAsChild() { 127 | assertObserverIsAddedAsChild(when: { sut in 128 | sut.onViewDidDisappear {} 129 | }) 130 | } 131 | 132 | func testViewDidDisappearObserverViewIsAddedAsSubview() { 133 | assertObserverViewIsAddedAsSubview(when: { sut in 134 | sut.onViewDidDisappear {} 135 | }) 136 | } 137 | 138 | func testViewDidDisappearObserverViewIsInvisible() { 139 | assertObserverViewIsInvisible(when: { sut in 140 | sut.onViewDidDisappear {} 141 | }) 142 | } 143 | 144 | func testViewDidDisappearObserverFiresCallback() { 145 | assertObserver( 146 | firesCallback: { $0.onViewDidDisappear }, 147 | when: { $0.viewDidDisappear(false) }) 148 | } 149 | 150 | func testCanRemoveViewDidDisappearObserver() { 151 | assertCanRemoveObserver(when: { sut in 152 | sut.onViewDidDisappear {} 153 | }) 154 | } 155 | 156 | func testCanRemoveViewDidDisappearObserverView() { 157 | assertCanRemoveObserverView(when: { sut in 158 | sut.onViewDidDisappear {} 159 | }) 160 | } 161 | 162 | // MARK: Integration Tests 163 | 164 | func testObserversWorkingWithNavigationControllerAnimatedTransitions() { 165 | let navigation = UINavigationController() 166 | let window = UIWindow(frame: UIScreen.main.bounds) 167 | window.rootViewController = navigation 168 | window.makeKeyAndVisible() 169 | 170 | let exp = expectation(description: "Wait for lifecycle callbacks") 171 | let view = UIViewController() 172 | 173 | view.onViewWillAppear { [weak view, weak navigation] in 174 | view?.onViewDidAppear { 175 | view?.onViewWillDisappear { 176 | view?.onViewDidDisappear { 177 | exp.fulfill() 178 | } 179 | } 180 | 181 | navigation?.setViewControllers([], animated: true) 182 | } 183 | } 184 | 185 | navigation.pushViewController(view, animated: true) 186 | wait(for: [exp], timeout: 1) 187 | } 188 | 189 | func testObserversWorkingWithNavigationControllerNonAnimatedTransitions() { 190 | let navigation = UINavigationController() 191 | let window = UIWindow(frame: UIScreen.main.bounds) 192 | window.rootViewController = navigation 193 | window.makeKeyAndVisible() 194 | 195 | let exp = expectation(description: "Wait for lifecycle callbacks") 196 | let view = UIViewController() 197 | 198 | view.onViewWillAppear { [weak view, weak navigation] in 199 | view?.onViewDidAppear { 200 | view?.onViewWillDisappear { 201 | view?.onViewDidDisappear { 202 | exp.fulfill() 203 | } 204 | } 205 | 206 | navigation?.setViewControllers([], animated: false) 207 | } 208 | } 209 | 210 | navigation.pushViewController(view, animated: false) 211 | wait(for: [exp], timeout: 1) 212 | } 213 | 214 | // MARK: Helpers 215 | 216 | func assertObserverIsAddedAsChild(when action: @escaping (UIViewController) -> Void, file: StaticString = #file, line: UInt = #line) { 217 | let sut = UIViewController() 218 | 219 | action(sut) 220 | 221 | XCTAssertEqual(sut.children.count, 1, file: file, line: line) 222 | } 223 | 224 | func assertObserverViewIsAddedAsSubview(when action: @escaping (UIViewController) -> Void, file: StaticString = #file, line: UInt = #line) { 225 | let sut = UIViewController() 226 | 227 | action(sut) 228 | 229 | let observer = sut.children.first 230 | XCTAssertEqual(observer?.view.superview, sut.view, file: file, line: line) 231 | } 232 | 233 | func assertObserverViewIsInvisible(when action: @escaping (UIViewController) -> Void, file: StaticString = #file, line: UInt = #line) { 234 | let sut = UIViewController() 235 | 236 | action(sut) 237 | 238 | let observer = sut.children.first 239 | XCTAssertEqual(observer?.view?.isHidden, true, file: file, line: line) 240 | } 241 | 242 | func assertObserver( 243 | firesCallback callback: (UIViewController) -> ((@escaping () -> Void) -> Observer), when action: @escaping (UIViewController) -> Void, file: StaticString = #file, line: UInt = #line) { 244 | let sut = UIViewController() 245 | 246 | var callCount = 0 247 | _ = callback(sut)({ callCount += 1 }) 248 | 249 | let observer = sut.children.first! 250 | XCTAssertEqual(callCount, 0, file: file, line: line) 251 | 252 | action(observer) 253 | XCTAssertEqual(callCount, 1, file: file, line: line) 254 | 255 | action(observer) 256 | XCTAssertEqual(callCount, 2, file: file, line: line) 257 | } 258 | 259 | func assertCanRemoveObserver(when action: @escaping (UIViewController) -> Observer, file: StaticString = #file, line: UInt = #line) { 260 | let sut = UIViewController() 261 | 262 | action(sut).remove() 263 | 264 | XCTAssertEqual(sut.children.count, 0, file: file, line: line) 265 | } 266 | 267 | func assertCanRemoveObserverView(when action: @escaping (UIViewController) -> Observer, file: StaticString = #file, line: UInt = #line) { 268 | let sut = UIViewController() 269 | 270 | action(sut).remove() 271 | 272 | XCTAssertEqual(sut.view.subviews.count, 0, file: file, line: line) 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | ignore: 3 | - "*Tests/*" 4 | 5 | range: "85...100" 6 | round: down 7 | precision: 2 8 | 9 | status: 10 | project: yes 11 | 12 | patch: 13 | default: 14 | enabled: yes # must be yes|true to enable this status 15 | target: 90% # specify the target "X%" coverage to hit 16 | branches: null # -> see "branch patterns" below 17 | threshold: null # allowed to drop X% and still result in a "success" commit status 18 | if_no_uploads: error # will post commit status of "error" if no coverage reports we uploaded 19 | if_not_found: success 20 | if_ci_failed: error 21 | 22 | changes: yes 23 | 24 | --------------------------------------------------------------------------------