├── .github └── workflows │ └── build.yml ├── .gitignore ├── .jazzy.yaml ├── .swiftlint.yml ├── Example ├── StateViewControllerExample.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── StateViewControllerExample.xcscheme └── StateViewControllerExample │ ├── ActivityIndicatorViewController.swift │ ├── AppDelegate.swift │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── Comment.swift │ ├── Info.plist │ ├── ListStateViewController.swift │ └── TableViewController.swift ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── StateViewController │ ├── StateViewController.swift │ └── StateViewControllerTransitioning.swift ├── StateViewController.xcodeproj ├── StateViewController_Info.plist ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings └── xcshareddata │ └── xcschemes │ └── StateViewController-Package.xcscheme ├── docs ├── Classes.html ├── Classes │ └── StateViewController.html ├── Protocols.html ├── Protocols │ └── StateViewControllerTransitioning.html ├── badge.svg ├── css │ ├── highlight.css │ └── jazzy.css ├── docsets │ ├── .docset │ │ └── Contents │ │ │ ├── Info.plist │ │ │ └── Resources │ │ │ ├── Documents │ │ │ ├── Classes.html │ │ │ ├── Classes │ │ │ │ └── StateViewController.html │ │ │ ├── Protocols.html │ │ │ ├── Protocols │ │ │ │ └── StateViewControllerTransitioning.html │ │ │ ├── css │ │ │ │ ├── highlight.css │ │ │ │ └── jazzy.css │ │ │ ├── img │ │ │ │ ├── carat.png │ │ │ │ ├── dash.png │ │ │ │ ├── gh.png │ │ │ │ └── spinner.gif │ │ │ ├── index.html │ │ │ ├── js │ │ │ │ ├── jazzy.js │ │ │ │ ├── jazzy.search.js │ │ │ │ ├── jquery.min.js │ │ │ │ ├── lunr.min.js │ │ │ │ └── typeahead.jquery.js │ │ │ └── search.json │ │ │ └── docSet.dsidx │ └── .tgz ├── img │ ├── carat.png │ ├── dash.png │ ├── gh.png │ └── spinner.gif ├── index.html ├── js │ ├── jazzy.js │ ├── jazzy.search.js │ ├── jquery.min.js │ ├── lunr.min.js │ └── typeahead.jquery.js ├── search.json └── undocumented.json └── images ├── between-lifecycle.png └── during-lifecycle.png /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | - push 4 | - pull_request 5 | 6 | jobs: 7 | iOS: 8 | name: Test iOS 9 | runs-on: macOS-latest 10 | env: 11 | DEVELOPER_DIR: /Applications/Xcode_11.4.1.app/Contents/Developer 12 | strategy: 13 | matrix: 14 | destination: ["OS=13.4.1,name=iPhone 11 Pro"] #, "OS=12.4,name=iPhone XS", "OS=11.4,name=iPhone X", "OS=10.3.1,name=iPhone SE"] 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: iOS - ${{ matrix.destination }} 18 | run: set -o pipefail && env NSUnbufferedIO=YES xcodebuild -project "StateViewController.xcodeproj" -scheme "StateViewController-Package" -destination "${{ matrix.destination }}" clean build | xcpretty 19 | tvOS: 20 | name: Test tvOS 21 | runs-on: macOS-latest 22 | env: 23 | DEVELOPER_DIR: /Applications/Xcode_11.4.1.app/Contents/Developer 24 | strategy: 25 | matrix: 26 | destination: ["OS=13.4,name=Apple TV 4K"] #, "OS=11.4,name=Apple TV 4K", "OS=10.2,name=Apple TV 1080p"] 27 | steps: 28 | - uses: actions/checkout@v2 29 | - name: tvOS - ${{ matrix.destination }} 30 | run: set -o pipefail && env NSUnbufferedIO=YES xcodebuild -project "StateViewController.xcodeproj" -scheme "StateViewController-Package" -destination "${{ matrix.destination }}" clean build | xcpretty -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Build generated 2 | build/ 3 | DerivedData/ 4 | 5 | ## Various settings 6 | *.pbxuser 7 | !default.pbxuser 8 | *.mode1v3 9 | !default.mode1v3 10 | *.mode2v3 11 | !default.mode2v3 12 | *.perspectivev3 13 | !default.perspectivev3 14 | xcuserdata/ 15 | 16 | ## Other 17 | *.moved-aside 18 | *.xccheckout 19 | *.xcscmblueprint 20 | 21 | ## Obj-C/Swift specific 22 | *.hmap 23 | *.ipa 24 | *.dSYM.zip 25 | *.dSYM 26 | 27 | ## Playgrounds 28 | timeline.xctimeline 29 | playground.xcworkspace 30 | 31 | # Swift Package Manager 32 | Packages/ 33 | Package.pins 34 | Package.resolved 35 | .build/ 36 | 37 | # Carthage 38 | Carthage/Checkouts 39 | Carthage/Build -------------------------------------------------------------------------------- /.jazzy.yaml: -------------------------------------------------------------------------------- 1 | output: docs 2 | clean: true 3 | exclude: [Packages, Carthage] 4 | author: David Ask 5 | github_url: https://github.com/davidask/StateViewController 6 | github_file_prefix: https://github.com/davidask/StateViewController/tree/master 7 | readme: README.md 8 | theme: fullwidth -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | excluded: 2 | - Carthage 3 | 4 | file_length: 5 | ignore_comment_only_lines: true 6 | 7 | identifier_name: 8 | min_length: 2 9 | validates_start_with_lowercase: false 10 | allowed_symbols: 11 | - "_" 12 | 13 | line_length: 14 | ignores_comments: true 15 | 16 | type_name: 17 | min_length: 2 18 | 19 | disabled_rules: 20 | - identifier_name 21 | 22 | nesting: 23 | type_level: 24 | warning: 3 25 | statement_level: 26 | warning: 10 27 | 28 | analyzer_rules: 29 | - unused_declaration 30 | - unused_import 31 | opt_in_rules: 32 | - anyobject_protocol 33 | - array_init 34 | - attributes 35 | - closure_end_indentation 36 | - closure_spacing 37 | - collection_alignment 38 | - contains_over_filter_count 39 | - contains_over_filter_is_empty 40 | - contains_over_first_not_nil 41 | - discouraged_object_literal 42 | - empty_count 43 | - empty_string 44 | - empty_xctest_method 45 | - explicit_init 46 | - extension_access_modifier 47 | - fallthrough 48 | - fatal_error_message 49 | - file_name 50 | - first_where 51 | - flatmap_over_map_reduce 52 | - identical_operands 53 | - joined_default_parameter 54 | - legacy_random 55 | - let_var_whitespace 56 | - last_where 57 | - literal_expression_end_indentation 58 | - lower_acl_than_parent 59 | - modifier_order 60 | - nimble_operator 61 | - nslocalizedstring_key 62 | - number_separator 63 | - object_literal 64 | - operator_usage_whitespace 65 | - overridden_super_call 66 | - override_in_extension 67 | - pattern_matching_keywords 68 | - private_action 69 | - private_outlet 70 | - prohibited_super_call 71 | - quick_discouraged_call 72 | - quick_discouraged_focused_test 73 | - quick_discouraged_pending_test 74 | - reduce_into 75 | - redundant_nil_coalescing 76 | - redundant_type_annotation 77 | - single_test_class 78 | - sorted_first_last 79 | - sorted_imports 80 | - static_operator 81 | - strong_iboutlet 82 | - toggle_bool 83 | - unavailable_function 84 | - unneeded_parentheses_in_closure_argument 85 | - unowned_variable_capture 86 | - untyped_error_in_catch 87 | - vertical_parameter_alignment_on_call 88 | - vertical_whitespace_closing_braces 89 | - xct_specific_matcher 90 | - yoda_condition -------------------------------------------------------------------------------- /Example/StateViewControllerExample.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | B76FCF8D24DBECFE0014697B /* StateViewController.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B76FCF8B24DBECFA0014697B /* StateViewController.framework */; }; 11 | B76FCF8E24DBECFE0014697B /* StateViewController.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B76FCF8B24DBECFA0014697B /* StateViewController.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 12 | B77C0EEC2125ED3700E88C7E /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B77C0EEB2125ED3700E88C7E /* AppDelegate.swift */; }; 13 | B77C0EEE2125ED3700E88C7E /* TableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B77C0EED2125ED3700E88C7E /* TableViewController.swift */; }; 14 | B77C0EF12125ED3800E88C7E /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B77C0EEF2125ED3800E88C7E /* Main.storyboard */; }; 15 | B77C0EF32125ED3900E88C7E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B77C0EF22125ED3900E88C7E /* Assets.xcassets */; }; 16 | B77C0EF62125ED3900E88C7E /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B77C0EF42125ED3900E88C7E /* LaunchScreen.storyboard */; }; 17 | B77C0F0C2125EF4000E88C7E /* ListStateViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B77C0F0B2125EF4000E88C7E /* ListStateViewController.swift */; }; 18 | B77C0F0E2125EFC300E88C7E /* ActivityIndicatorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B77C0F0D2125EFC300E88C7E /* ActivityIndicatorViewController.swift */; }; 19 | B77C0F102125F14400E88C7E /* Comment.swift in Sources */ = {isa = PBXBuildFile; fileRef = B77C0F0F2125F14400E88C7E /* Comment.swift */; }; 20 | /* End PBXBuildFile section */ 21 | 22 | /* Begin PBXContainerItemProxy section */ 23 | B76FCF8A24DBECFA0014697B /* PBXContainerItemProxy */ = { 24 | isa = PBXContainerItemProxy; 25 | containerPortal = B77C0EFD2125ED5800E88C7E /* StateViewController.xcodeproj */; 26 | proxyType = 2; 27 | remoteGlobalIDString = "StateViewController::StateViewController::Product"; 28 | remoteInfo = StateViewController; 29 | }; 30 | /* End PBXContainerItemProxy section */ 31 | 32 | /* Begin PBXCopyFilesBuildPhase section */ 33 | B76FCF8F24DBECFE0014697B /* Embed Frameworks */ = { 34 | isa = PBXCopyFilesBuildPhase; 35 | buildActionMask = 2147483647; 36 | dstPath = ""; 37 | dstSubfolderSpec = 10; 38 | files = ( 39 | B76FCF8E24DBECFE0014697B /* StateViewController.framework in Embed Frameworks */, 40 | ); 41 | name = "Embed Frameworks"; 42 | runOnlyForDeploymentPostprocessing = 0; 43 | }; 44 | /* End PBXCopyFilesBuildPhase section */ 45 | 46 | /* Begin PBXFileReference section */ 47 | B77C0EE82125ED3700E88C7E /* StateViewControllerExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = StateViewControllerExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 48 | B77C0EEB2125ED3700E88C7E /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 49 | B77C0EED2125ED3700E88C7E /* TableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewController.swift; sourceTree = ""; }; 50 | B77C0EF02125ED3800E88C7E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 51 | B77C0EF22125ED3900E88C7E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 52 | B77C0EF52125ED3900E88C7E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 53 | B77C0EF72125ED3900E88C7E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 54 | B77C0EFD2125ED5800E88C7E /* StateViewController.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = StateViewController.xcodeproj; path = ../StateViewController.xcodeproj; sourceTree = ""; }; 55 | B77C0F0B2125EF4000E88C7E /* ListStateViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListStateViewController.swift; sourceTree = ""; }; 56 | B77C0F0D2125EFC300E88C7E /* ActivityIndicatorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityIndicatorViewController.swift; sourceTree = ""; }; 57 | B77C0F0F2125F14400E88C7E /* Comment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Comment.swift; sourceTree = ""; }; 58 | /* End PBXFileReference section */ 59 | 60 | /* Begin PBXFrameworksBuildPhase section */ 61 | B77C0EE52125ED3700E88C7E /* Frameworks */ = { 62 | isa = PBXFrameworksBuildPhase; 63 | buildActionMask = 2147483647; 64 | files = ( 65 | B76FCF8D24DBECFE0014697B /* StateViewController.framework in Frameworks */, 66 | ); 67 | runOnlyForDeploymentPostprocessing = 0; 68 | }; 69 | /* End PBXFrameworksBuildPhase section */ 70 | 71 | /* Begin PBXGroup section */ 72 | B76FCF8C24DBECFE0014697B /* Frameworks */ = { 73 | isa = PBXGroup; 74 | children = ( 75 | ); 76 | name = Frameworks; 77 | sourceTree = ""; 78 | }; 79 | B77C0EDF2125ED3700E88C7E = { 80 | isa = PBXGroup; 81 | children = ( 82 | B77C0EFD2125ED5800E88C7E /* StateViewController.xcodeproj */, 83 | B77C0EEA2125ED3700E88C7E /* StateViewControllerExample */, 84 | B77C0EE92125ED3700E88C7E /* Products */, 85 | B76FCF8C24DBECFE0014697B /* Frameworks */, 86 | ); 87 | sourceTree = ""; 88 | }; 89 | B77C0EE92125ED3700E88C7E /* Products */ = { 90 | isa = PBXGroup; 91 | children = ( 92 | B77C0EE82125ED3700E88C7E /* StateViewControllerExample.app */, 93 | ); 94 | name = Products; 95 | sourceTree = ""; 96 | }; 97 | B77C0EEA2125ED3700E88C7E /* StateViewControllerExample */ = { 98 | isa = PBXGroup; 99 | children = ( 100 | B77C0EEB2125ED3700E88C7E /* AppDelegate.swift */, 101 | B77C0EED2125ED3700E88C7E /* TableViewController.swift */, 102 | B77C0F0B2125EF4000E88C7E /* ListStateViewController.swift */, 103 | B77C0F0D2125EFC300E88C7E /* ActivityIndicatorViewController.swift */, 104 | B77C0EEF2125ED3800E88C7E /* Main.storyboard */, 105 | B77C0EF22125ED3900E88C7E /* Assets.xcassets */, 106 | B77C0EF42125ED3900E88C7E /* LaunchScreen.storyboard */, 107 | B77C0EF72125ED3900E88C7E /* Info.plist */, 108 | B77C0F0F2125F14400E88C7E /* Comment.swift */, 109 | ); 110 | path = StateViewControllerExample; 111 | sourceTree = ""; 112 | }; 113 | B77C0EFE2125ED5800E88C7E /* Products */ = { 114 | isa = PBXGroup; 115 | children = ( 116 | B76FCF8B24DBECFA0014697B /* StateViewController.framework */, 117 | ); 118 | name = Products; 119 | sourceTree = ""; 120 | }; 121 | /* End PBXGroup section */ 122 | 123 | /* Begin PBXNativeTarget section */ 124 | B77C0EE72125ED3700E88C7E /* StateViewControllerExample */ = { 125 | isa = PBXNativeTarget; 126 | buildConfigurationList = B77C0EFA2125ED3900E88C7E /* Build configuration list for PBXNativeTarget "StateViewControllerExample" */; 127 | buildPhases = ( 128 | B77C0EE42125ED3700E88C7E /* Sources */, 129 | B77C0EE52125ED3700E88C7E /* Frameworks */, 130 | B77C0EE62125ED3700E88C7E /* Resources */, 131 | B76FCF8F24DBECFE0014697B /* Embed Frameworks */, 132 | ); 133 | buildRules = ( 134 | ); 135 | dependencies = ( 136 | ); 137 | name = StateViewControllerExample; 138 | productName = StateViewControllerExample; 139 | productReference = B77C0EE82125ED3700E88C7E /* StateViewControllerExample.app */; 140 | productType = "com.apple.product-type.application"; 141 | }; 142 | /* End PBXNativeTarget section */ 143 | 144 | /* Begin PBXProject section */ 145 | B77C0EE02125ED3700E88C7E /* Project object */ = { 146 | isa = PBXProject; 147 | attributes = { 148 | LastSwiftUpdateCheck = 0940; 149 | LastUpgradeCheck = 0940; 150 | ORGANIZATIONNAME = Formbound; 151 | TargetAttributes = { 152 | B77C0EE72125ED3700E88C7E = { 153 | CreatedOnToolsVersion = 9.4.1; 154 | LastSwiftMigration = 1150; 155 | }; 156 | }; 157 | }; 158 | buildConfigurationList = B77C0EE32125ED3700E88C7E /* Build configuration list for PBXProject "StateViewControllerExample" */; 159 | compatibilityVersion = "Xcode 9.3"; 160 | developmentRegion = en; 161 | hasScannedForEncodings = 0; 162 | knownRegions = ( 163 | en, 164 | Base, 165 | ); 166 | mainGroup = B77C0EDF2125ED3700E88C7E; 167 | productRefGroup = B77C0EE92125ED3700E88C7E /* Products */; 168 | projectDirPath = ""; 169 | projectReferences = ( 170 | { 171 | ProductGroup = B77C0EFE2125ED5800E88C7E /* Products */; 172 | ProjectRef = B77C0EFD2125ED5800E88C7E /* StateViewController.xcodeproj */; 173 | }, 174 | ); 175 | projectRoot = ""; 176 | targets = ( 177 | B77C0EE72125ED3700E88C7E /* StateViewControllerExample */, 178 | ); 179 | }; 180 | /* End PBXProject section */ 181 | 182 | /* Begin PBXReferenceProxy section */ 183 | B76FCF8B24DBECFA0014697B /* StateViewController.framework */ = { 184 | isa = PBXReferenceProxy; 185 | fileType = wrapper.framework; 186 | path = StateViewController.framework; 187 | remoteRef = B76FCF8A24DBECFA0014697B /* PBXContainerItemProxy */; 188 | sourceTree = BUILT_PRODUCTS_DIR; 189 | }; 190 | /* End PBXReferenceProxy section */ 191 | 192 | /* Begin PBXResourcesBuildPhase section */ 193 | B77C0EE62125ED3700E88C7E /* Resources */ = { 194 | isa = PBXResourcesBuildPhase; 195 | buildActionMask = 2147483647; 196 | files = ( 197 | B77C0EF62125ED3900E88C7E /* LaunchScreen.storyboard in Resources */, 198 | B77C0EF32125ED3900E88C7E /* Assets.xcassets in Resources */, 199 | B77C0EF12125ED3800E88C7E /* Main.storyboard in Resources */, 200 | ); 201 | runOnlyForDeploymentPostprocessing = 0; 202 | }; 203 | /* End PBXResourcesBuildPhase section */ 204 | 205 | /* Begin PBXSourcesBuildPhase section */ 206 | B77C0EE42125ED3700E88C7E /* Sources */ = { 207 | isa = PBXSourcesBuildPhase; 208 | buildActionMask = 2147483647; 209 | files = ( 210 | B77C0F102125F14400E88C7E /* Comment.swift in Sources */, 211 | B77C0EEE2125ED3700E88C7E /* TableViewController.swift in Sources */, 212 | B77C0F0E2125EFC300E88C7E /* ActivityIndicatorViewController.swift in Sources */, 213 | B77C0EEC2125ED3700E88C7E /* AppDelegate.swift in Sources */, 214 | B77C0F0C2125EF4000E88C7E /* ListStateViewController.swift in Sources */, 215 | ); 216 | runOnlyForDeploymentPostprocessing = 0; 217 | }; 218 | /* End PBXSourcesBuildPhase section */ 219 | 220 | /* Begin PBXVariantGroup section */ 221 | B77C0EEF2125ED3800E88C7E /* Main.storyboard */ = { 222 | isa = PBXVariantGroup; 223 | children = ( 224 | B77C0EF02125ED3800E88C7E /* Base */, 225 | ); 226 | name = Main.storyboard; 227 | sourceTree = ""; 228 | }; 229 | B77C0EF42125ED3900E88C7E /* LaunchScreen.storyboard */ = { 230 | isa = PBXVariantGroup; 231 | children = ( 232 | B77C0EF52125ED3900E88C7E /* Base */, 233 | ); 234 | name = LaunchScreen.storyboard; 235 | sourceTree = ""; 236 | }; 237 | /* End PBXVariantGroup section */ 238 | 239 | /* Begin XCBuildConfiguration section */ 240 | B77C0EF82125ED3900E88C7E /* Debug */ = { 241 | isa = XCBuildConfiguration; 242 | buildSettings = { 243 | ALWAYS_SEARCH_USER_PATHS = NO; 244 | CLANG_ANALYZER_NONNULL = YES; 245 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 246 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 247 | CLANG_CXX_LIBRARY = "libc++"; 248 | CLANG_ENABLE_MODULES = YES; 249 | CLANG_ENABLE_OBJC_ARC = YES; 250 | CLANG_ENABLE_OBJC_WEAK = YES; 251 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 252 | CLANG_WARN_BOOL_CONVERSION = YES; 253 | CLANG_WARN_COMMA = YES; 254 | CLANG_WARN_CONSTANT_CONVERSION = YES; 255 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 256 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 257 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 258 | CLANG_WARN_EMPTY_BODY = YES; 259 | CLANG_WARN_ENUM_CONVERSION = YES; 260 | CLANG_WARN_INFINITE_RECURSION = YES; 261 | CLANG_WARN_INT_CONVERSION = YES; 262 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 263 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 264 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 265 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 266 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 267 | CLANG_WARN_STRICT_PROTOTYPES = YES; 268 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 269 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 270 | CLANG_WARN_UNREACHABLE_CODE = YES; 271 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 272 | CODE_SIGN_IDENTITY = "iPhone Developer"; 273 | COPY_PHASE_STRIP = NO; 274 | DEBUG_INFORMATION_FORMAT = dwarf; 275 | ENABLE_STRICT_OBJC_MSGSEND = YES; 276 | ENABLE_TESTABILITY = YES; 277 | GCC_C_LANGUAGE_STANDARD = gnu11; 278 | GCC_DYNAMIC_NO_PIC = NO; 279 | GCC_NO_COMMON_BLOCKS = YES; 280 | GCC_OPTIMIZATION_LEVEL = 0; 281 | GCC_PREPROCESSOR_DEFINITIONS = ( 282 | "DEBUG=1", 283 | "$(inherited)", 284 | ); 285 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 286 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 287 | GCC_WARN_UNDECLARED_SELECTOR = YES; 288 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 289 | GCC_WARN_UNUSED_FUNCTION = YES; 290 | GCC_WARN_UNUSED_VARIABLE = YES; 291 | IPHONEOS_DEPLOYMENT_TARGET = 11.4; 292 | MTL_ENABLE_DEBUG_INFO = YES; 293 | ONLY_ACTIVE_ARCH = YES; 294 | SDKROOT = iphoneos; 295 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 296 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 297 | }; 298 | name = Debug; 299 | }; 300 | B77C0EF92125ED3900E88C7E /* Release */ = { 301 | isa = XCBuildConfiguration; 302 | buildSettings = { 303 | ALWAYS_SEARCH_USER_PATHS = NO; 304 | CLANG_ANALYZER_NONNULL = YES; 305 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 306 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 307 | CLANG_CXX_LIBRARY = "libc++"; 308 | CLANG_ENABLE_MODULES = YES; 309 | CLANG_ENABLE_OBJC_ARC = YES; 310 | CLANG_ENABLE_OBJC_WEAK = YES; 311 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 312 | CLANG_WARN_BOOL_CONVERSION = YES; 313 | CLANG_WARN_COMMA = YES; 314 | CLANG_WARN_CONSTANT_CONVERSION = YES; 315 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 316 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 317 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 318 | CLANG_WARN_EMPTY_BODY = YES; 319 | CLANG_WARN_ENUM_CONVERSION = YES; 320 | CLANG_WARN_INFINITE_RECURSION = YES; 321 | CLANG_WARN_INT_CONVERSION = YES; 322 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 323 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 324 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 325 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 326 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 327 | CLANG_WARN_STRICT_PROTOTYPES = YES; 328 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 329 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 330 | CLANG_WARN_UNREACHABLE_CODE = YES; 331 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 332 | CODE_SIGN_IDENTITY = "iPhone Developer"; 333 | COPY_PHASE_STRIP = NO; 334 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 335 | ENABLE_NS_ASSERTIONS = NO; 336 | ENABLE_STRICT_OBJC_MSGSEND = YES; 337 | GCC_C_LANGUAGE_STANDARD = gnu11; 338 | GCC_NO_COMMON_BLOCKS = YES; 339 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 340 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 341 | GCC_WARN_UNDECLARED_SELECTOR = YES; 342 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 343 | GCC_WARN_UNUSED_FUNCTION = YES; 344 | GCC_WARN_UNUSED_VARIABLE = YES; 345 | IPHONEOS_DEPLOYMENT_TARGET = 11.4; 346 | MTL_ENABLE_DEBUG_INFO = NO; 347 | SDKROOT = iphoneos; 348 | SWIFT_COMPILATION_MODE = wholemodule; 349 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 350 | VALIDATE_PRODUCT = YES; 351 | }; 352 | name = Release; 353 | }; 354 | B77C0EFB2125ED3900E88C7E /* Debug */ = { 355 | isa = XCBuildConfiguration; 356 | buildSettings = { 357 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 358 | CODE_SIGN_STYLE = Automatic; 359 | DEVELOPMENT_TEAM = ""; 360 | INFOPLIST_FILE = StateViewControllerExample/Info.plist; 361 | LD_RUNPATH_SEARCH_PATHS = ( 362 | "$(inherited)", 363 | "@executable_path/Frameworks", 364 | ); 365 | PRODUCT_BUNDLE_IDENTIFIER = com.formbound.StateViewControllerExample; 366 | PRODUCT_NAME = "$(TARGET_NAME)"; 367 | SWIFT_VERSION = 5.0; 368 | TARGETED_DEVICE_FAMILY = "1,2"; 369 | }; 370 | name = Debug; 371 | }; 372 | B77C0EFC2125ED3900E88C7E /* Release */ = { 373 | isa = XCBuildConfiguration; 374 | buildSettings = { 375 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 376 | CODE_SIGN_STYLE = Automatic; 377 | DEVELOPMENT_TEAM = ""; 378 | INFOPLIST_FILE = StateViewControllerExample/Info.plist; 379 | LD_RUNPATH_SEARCH_PATHS = ( 380 | "$(inherited)", 381 | "@executable_path/Frameworks", 382 | ); 383 | PRODUCT_BUNDLE_IDENTIFIER = com.formbound.StateViewControllerExample; 384 | PRODUCT_NAME = "$(TARGET_NAME)"; 385 | SWIFT_VERSION = 5.0; 386 | TARGETED_DEVICE_FAMILY = "1,2"; 387 | }; 388 | name = Release; 389 | }; 390 | /* End XCBuildConfiguration section */ 391 | 392 | /* Begin XCConfigurationList section */ 393 | B77C0EE32125ED3700E88C7E /* Build configuration list for PBXProject "StateViewControllerExample" */ = { 394 | isa = XCConfigurationList; 395 | buildConfigurations = ( 396 | B77C0EF82125ED3900E88C7E /* Debug */, 397 | B77C0EF92125ED3900E88C7E /* Release */, 398 | ); 399 | defaultConfigurationIsVisible = 0; 400 | defaultConfigurationName = Release; 401 | }; 402 | B77C0EFA2125ED3900E88C7E /* Build configuration list for PBXNativeTarget "StateViewControllerExample" */ = { 403 | isa = XCConfigurationList; 404 | buildConfigurations = ( 405 | B77C0EFB2125ED3900E88C7E /* Debug */, 406 | B77C0EFC2125ED3900E88C7E /* Release */, 407 | ); 408 | defaultConfigurationIsVisible = 0; 409 | defaultConfigurationName = Release; 410 | }; 411 | /* End XCConfigurationList section */ 412 | }; 413 | rootObject = B77C0EE02125ED3700E88C7E /* Project object */; 414 | } 415 | -------------------------------------------------------------------------------- /Example/StateViewControllerExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/StateViewControllerExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/StateViewControllerExample.xcodeproj/xcshareddata/xcschemes/StateViewControllerExample.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /Example/StateViewControllerExample/ActivityIndicatorViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActivityIndicatorViewController.swift 3 | // StateViewControllerExample 4 | // 5 | // Created by David Ask on 2018-08-16. 6 | // Copyright © 2018 Formbound. All rights reserved. 7 | // 8 | 9 | import StateViewController 10 | import UIKit 11 | 12 | class ActivityIndicatorViewController: UIViewController { 13 | 14 | @IBOutlet private var activityIndicator: UIActivityIndicatorView! 15 | 16 | @IBOutlet private var activityIndicatorContainer: UIView! 17 | 18 | override func viewDidLoad() { 19 | super.viewDidLoad() 20 | activityIndicatorContainer.layer.cornerRadius = 5 21 | } 22 | 23 | override func viewWillAppear(_ animated: Bool) { 24 | super.viewWillAppear(animated) 25 | 26 | activityIndicator.startAnimating() 27 | } 28 | 29 | override func viewDidDisappear(_ animated: Bool) { 30 | super.viewDidDisappear(animated) 31 | 32 | activityIndicator.stopAnimating() 33 | } 34 | } 35 | 36 | extension ActivityIndicatorViewController: StateViewControllerTransitioning { 37 | 38 | func stateTransitionDuration(isAppearing: Bool) -> TimeInterval { 39 | return 0.5 40 | } 41 | 42 | func stateTransitionWillBegin(isAppearing: Bool) { 43 | if isAppearing { 44 | view.alpha = 0 45 | activityIndicatorContainer.transform = CGAffineTransform 46 | .identity 47 | .scaledBy(x: 0.5, y: 0.5) 48 | } 49 | } 50 | 51 | func stateTransitionDidEnd(isAppearing: Bool) { 52 | view.alpha = 1 53 | activityIndicatorContainer.transform = .identity 54 | } 55 | 56 | func animateAlongsideStateTransition(isAppearing: Bool) { 57 | if isAppearing { 58 | view.alpha = 1 59 | activityIndicatorContainer.transform = .identity 60 | } else { 61 | view.alpha = 0 62 | activityIndicatorContainer.transform = CGAffineTransform 63 | .identity 64 | .scaledBy(x: 1.5, y: 1.5) 65 | } 66 | } 67 | 68 | func stateTransitionDelay(isAppearing: Bool) -> TimeInterval { 69 | return isAppearing ? 0 : 0.5 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Example/StateViewControllerExample/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // StateViewControllerExample 4 | // 5 | // Created by David Ask on 2018-08-16. 6 | // Copyright © 2018 Formbound. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | var window: UIWindow? 14 | } 15 | -------------------------------------------------------------------------------- /Example/StateViewControllerExample/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 | } -------------------------------------------------------------------------------- /Example/StateViewControllerExample/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Example/StateViewControllerExample/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 | -------------------------------------------------------------------------------- /Example/StateViewControllerExample/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 50 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | -------------------------------------------------------------------------------- /Example/StateViewControllerExample/Comment.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Comment.swift 3 | // StateViewControllerExample 4 | // 5 | // Created by David Ask on 2018-08-16. 6 | // Copyright © 2018 Formbound. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct Comment: Decodable { 12 | let id: Int 13 | let name: String 14 | let email: String 15 | let body: String 16 | 17 | static func loadComments(_ completion: @escaping (Error?, [Comment]?) -> Void) { 18 | 19 | let url = URL(string: "https://jsonplaceholder.typicode.com/comments")! 20 | 21 | let task = URLSession.shared.dataTask(with: url) { data, _, error in 22 | if let error = error { 23 | completion(error, nil) 24 | return 25 | } 26 | 27 | guard let data = data else { 28 | completion(nil, []) 29 | return 30 | } 31 | 32 | do { 33 | completion(nil, try JSONDecoder().decode([Comment].self, from: data)) 34 | } catch { 35 | completion(error, nil) 36 | } 37 | } 38 | 39 | task.resume() 40 | } 41 | } 42 | 43 | extension Comment: Equatable { 44 | static func == (lhs: Comment, rhs: Comment) -> Bool { 45 | return lhs.id == rhs.id 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Example/StateViewControllerExample/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 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /Example/StateViewControllerExample/ListStateViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ListStateViewController.swift 3 | // StateViewControllerExample 4 | // 5 | // Created by David Ask on 2018-08-16. 6 | // Copyright © 2018 Formbound. All rights reserved. 7 | // 8 | 9 | import StateViewController 10 | import UIKit 11 | 12 | enum ListStateViewControllerState: Equatable { 13 | case loading 14 | case ready([Comment]) 15 | } 16 | 17 | class ListStateViewController: StateViewController { 18 | 19 | lazy var reloadBarButtonItem = UIBarButtonItem( 20 | barButtonSystemItem: .refresh, 21 | target: self, 22 | action: #selector(refresh) 23 | ) 24 | 25 | override func awakeFromNib() { 26 | super.awakeFromNib() 27 | 28 | title = "Comments" 29 | } 30 | 31 | override func viewDidLoad() { 32 | super.viewDidLoad() 33 | } 34 | 35 | private lazy var tableViewController: TableViewController = { 36 | let storyboard = UIStoryboard(name: "Main", bundle: .main) 37 | // swiftlint:disable force_cast 38 | return storyboard.instantiateViewController(withIdentifier: "tableViewController") as! TableViewController 39 | // swiftlint:enable force_cast 40 | }() 41 | 42 | override func loadAppearanceState() -> ListStateViewControllerState { 43 | return .loading 44 | } 45 | 46 | override func children(for state: ListStateViewControllerState) -> [UIViewController] { 47 | switch state { 48 | case .loading: 49 | 50 | let storyboard = UIStoryboard(name: "Main", bundle: .main) 51 | 52 | return [ 53 | tableViewController, 54 | storyboard.instantiateViewController(withIdentifier: "activityIndicatorController") 55 | ] 56 | case .ready: 57 | return [ 58 | tableViewController 59 | ] 60 | } 61 | } 62 | 63 | override func willTransition(to nextState: ListStateViewControllerState, animated: Bool) { 64 | switch nextState { 65 | case .loading: 66 | navigationItem.setRightBarButton(nil, animated: animated) 67 | case .ready(let comments): 68 | navigationItem.setRightBarButton(reloadBarButtonItem, animated: animated) 69 | tableViewController.comments = comments 70 | } 71 | } 72 | 73 | override func didTransition(from previousState: ListStateViewControllerState?, animated: Bool) { 74 | switch currentState { 75 | case .loading: 76 | Comment.loadComments { _, comments in 77 | guard let comments = comments else { 78 | return 79 | } 80 | 81 | DispatchQueue.main.async { 82 | self.setNeedsStateTransition(to: .ready(comments), animated: true) 83 | } 84 | } 85 | case .ready: 86 | break 87 | } 88 | } 89 | 90 | @objc 91 | func refresh() { 92 | self.setNeedsStateTransition(to: .loading, animated: true) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /Example/StateViewControllerExample/TableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableViewController.swift 3 | // StateViewControllerExample 4 | // 5 | // Created by David Ask on 2018-08-16. 6 | // Copyright © 2018 Formbound. All rights reserved. 7 | // 8 | 9 | import StateViewController 10 | import UIKit 11 | 12 | class TableViewController: UITableViewController { 13 | 14 | var comments: [Comment] = [] { 15 | didSet { 16 | tableView.reloadData() 17 | } 18 | } 19 | 20 | override func viewDidLoad() { 21 | super.viewDidLoad() 22 | } 23 | 24 | override func numberOfSections(in tableView: UITableView) -> Int { 25 | return 1 26 | } 27 | 28 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 29 | return comments.count 30 | } 31 | 32 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 33 | return tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) 34 | } 35 | 36 | override func tableView( 37 | _ tableView: UITableView, 38 | willDisplay cell: UITableViewCell, 39 | forRowAt indexPath: IndexPath 40 | ) { 41 | 42 | let comment = comments[indexPath.row] 43 | 44 | cell.textLabel?.text = comment.name 45 | cell.detailTextLabel?.text = comment.body 46 | } 47 | } 48 | // 49 | // 50 | //extension TableViewController: StateViewControllerTransitioning { 51 | // 52 | // func stateTransitionDuration(isAppearing: Bool) -> TimeInterval { 53 | // return 0.25 54 | // } 55 | // 56 | // func stateTransitionWillBegin(isAppearing: Bool) { 57 | // if isAppearing { 58 | // view.alpha = 0 59 | // view.transform = CGAffineTransform.identity.scaledBy(x: 0.95, y: 0.95) 60 | // } 61 | // } 62 | // 63 | // func stateTransitionDidEnd(isAppearing: Bool) { 64 | // view.alpha = 1 65 | // view.transform = .identity 66 | // } 67 | // 68 | // func animateAlongsideStateTransition(isAppearing: Bool) { 69 | // if isAppearing { 70 | // view.alpha = 1 71 | // view.transform = .identity 72 | // } else { 73 | // view.alpha = 0 74 | // view.transform = CGAffineTransform.identity.scaledBy(x: 0.95, y: 0.95) 75 | // } 76 | // } 77 | // 78 | // func stateTransitionDelay(isAppearing: Bool) -> TimeInterval { 79 | // return 0 80 | // } 81 | //} 82 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 David Ask 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. -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.2 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "StateViewController", 7 | platforms: [ 8 | .iOS(.v8), 9 | .tvOS(.v9) 10 | ], 11 | products: [ 12 | .library( 13 | name: "StateViewController", 14 | targets: ["StateViewController"]) 15 | ], 16 | targets: [ 17 | .target( 18 | name: "StateViewController", 19 | dependencies: []) 20 | ] 21 | ) 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Build](https://github.com/davidask/StateViewController/workflows/Build/badge.svg) 2 | # StateViewController 3 | 4 | When creating rich view controllers, a single view controller class is often tasked with managing the appearance of many other views, controls, and other user interface elements based on a state. That state, in turn, is often derived from multiple sources that need to be synchronized to correctly represent a single reliable state. Usually the end result is known as the *Massive View Controller* problem, often solved by attempts to abandon the [MVC](https://developer.apple.com/library/archive/documentation/General/Conceptual/DevPedia-CocoaCore/MVC.html) pattern, the primary design pattern in UIKit. While other patterns, such as [MVVM](https://en.wikipedia.org/wiki/Model–view–viewmodel) or [MVP](https://en.wikipedia.org/wiki/Model–view–presenter), can solve some issues, going with the grain rather than against makes interacting with UIKit more accommodating. 5 | 6 | This repository houses a `UIViewController` subclass, enabling modularization and decoupling of view controllers, reducing the size of individual view controllers substantially, without the need for abandoning MVC as a design pattern. 7 | 8 | 9 | 10 | ## Requirements 11 | 12 | * iOS 8.0+ 13 | * tvOS 9.0+ 14 | 15 | ## Overview 16 | `StateViewController` is a container view controller that presents one or more view controllers for any given state that you define, such as `loading`, `list`, or `editing`. It manages the appearance cycles of each child view controller, making sure that the view life cycle of the child view controllers are intact and in order, notifying you about state transitions and which child view controllers are about to appear or disappear from the view hierarchy. This allows you to compose multiple view controllers and re-use them throughout the app. The state view controller also provides extensive support for animating the transition between states. 17 | 18 | Which view controller(s) are visible on screen is dictated by `children(for:)`. 19 | 20 | ### State transitions during appearance transition 21 | When presented on screen, the a state view controller requires an initial state as a starting point. During its appearance transition, the `loadAppearanceState()` method is invoked to query the state appropriate to transition to as the state view controller appears on screen. 22 | If the appearance transition is animated, the state transition animation is respected, and target child view controllers have the option to appear asynchronously. If the appearance transition is not animated, all child view controllers are immediately placed on screen. 23 | 24 | `loadAppearanceState()` must execute synchronously, and is a good place to query any persistence layer for available data, determining whether a final state is ready. 25 | 26 | 27 | ![During appearance cycle](https://raw.githubusercontent.com/davidask/StateViewController/master/images/during-lifecycle.png "StateViewController during appearance cycles") 28 | 29 | ### State transitions while on screen 30 | 31 | When on-screen, invoking `setNeedsTransition:to:` will trigger a transition from the current state to the target state. A common practice is to have the transition from one state to another to trigger an an asynchronous operation (such as a network call), which upon completion, requests a third state based on the success of the asynchronous operation. 32 | 33 | ![Between appearance cycle](https://raw.githubusercontent.com/davidask/StateViewController/master/images/between-lifecycle.png "StateViewController between appearance cycles") 34 | 35 | ## Documentation 36 | 37 | The source code documentation can be found [here](https://davidask.github.io/StateViewController/). 38 | 39 | ## Installation 40 | This module is available via [Carthage](https://github.com/Carthage/Carthage). Modify your [Cartfile](https://github.com/Carthage/Carthage#quick-start) to include `StateViewController`: 41 | 42 | ``` 43 | github "davidask/StateViewController" 44 | ``` 45 | 46 | ## Usage 47 | 48 | ```swift 49 | import StateViewController 50 | ``` 51 | 52 | ### Subclassing StateViewController 53 | 54 | To use `StateViewController` you must override it. The class specifies a generic with a subtype of `State`. The state type can be designed to house the actual model data required by your view controller, but that's an optional design decision. For instance, you can create a state that simply determines an abstract state: 55 | 56 | ```swift 57 | enum MyState { 58 | case loading 59 | case ready 60 | case error 61 | } 62 | ``` 63 | 64 | Or, you can define a state which in itself contains model data: 65 | ```swift 66 | enum MyState { 67 | case loading 68 | case ready(MyModel) 69 | case error(Error) 70 | } 71 | ``` 72 | 73 | Once you have a state, create a subclass of `StateViewController`. 74 | 75 | ```swift 76 | class MyStateViewController: StateViewController 77 | ``` 78 | 79 | Each time `StateViewController` is about to appear on screen it will call its `loadAppearanceState()` method. This method returns a state which should be ready for display as soon as the view controller is on screen. Override this method to determine what state is appropriate to display immediately, depending on cached data or the contents of your database. 80 | 81 | ```swift 82 | override func loadAppearanceState() -> MyState { 83 | if let myModel = MyCache.cachedModel { 84 | return .ready(myModel) 85 | } else { 86 | return .loading 87 | } 88 | } 89 | ``` 90 | 91 | Each state can be represented by zero or more view controllers. To provide which view controllers are visible for what state, override `children(for:)`. 92 | 93 | ```swift 94 | override func children(for state: MyState) -> [UIViewController] { 95 | switch state { 96 | case .loading: 97 | return [ActivityIndicatorViewController()] 98 | case .ready: 99 | return [myTableViewController] 100 | case .error: 101 | return [ErrorViewController()] 102 | } 103 | } 104 | ``` 105 | 106 | You receive callbacks for when a state transition will begin, and when it has finished. 107 | `willTransition(to:animated:)` is a good place to prepare your child view controllers for appearance. 108 | 109 | ```swift 110 | override func willTransition(to nextState: MyState, animated: Bool) { 111 | switch nextState { 112 | case .ready(let model): 113 | navigationItem.setRightBarButton(myBarButtonItem, animated: animated) 114 | myTableViewController.setRows(model.objects) 115 | default: 116 | navigationItem.setRightBarButton(nil, animated: animated) 117 | } 118 | } 119 | ``` 120 | 121 | When `didTransition(from:animated:)` is called, a state transition has finished successfully. This is a good time to invoke other methods which in turn will trigger another state transition. 122 | 123 | ```swift 124 | override func didTransition(from previousState: State?, animated: Bool) { 125 | switch currentState { 126 | case .loading: 127 | fetchData { model in 128 | self.setNeedsTransition(to: .ready(model), animated: true) 129 | } 130 | defualt: 131 | break 132 | } 133 | } 134 | ``` 135 | 136 | Your `StateViewController` is now ready, and will switch between view controllers depending on state. Using `setNeedsTransition(:to:animated:)` you can transition between various states during the life cycle of your state view controller subclass. 137 | 138 | Multiple other callbacks are available for determining when a child view controller is appearing or disappearing. Please reference the documentation or the [Example](/Example). 139 | 140 | 141 | ### Providing transitions between child view controllers 142 | 143 | Child view controllers of `StateViewController` conforming to the [`StateViewControllerTransitioning`](Sources/StateViewController/StateViewControllerTransitioning.swift) protocol can individually control their own transition. The available methods provide functionality for: 144 | 145 | - Specifying duration and delayed start of an animated transition 146 | - Preparing the view controller for presentation 147 | - Performing animations along side an animated transition 148 | - Performing operations after a transition 149 | 150 | 151 | ## Example 152 | 153 | In the example application included in this project the state view controller switches between two view controllers. Firstly, it displays and animates the transition of an activity indicator view controller while a network call is being performed. Once the network call is successfully completed it transitions into a state displaying a table view with the loaded content. 154 | 155 | ## Contribute 156 | 157 | Please feel welcome contributing to **StateViewController**, check the ``LICENSE`` file for more info. 158 | 159 | ## Credits 160 | 161 | David Ask 162 | -------------------------------------------------------------------------------- /Sources/StateViewController/StateViewControllerTransitioning.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) 2 | 3 | import UIKit 4 | 5 | /// View controllers can conform to this protocol to provide their desired 6 | /// state transitioning behaviour when contained in a `StateViewController`. 7 | public protocol StateViewControllerTransitioning: AnyObject { 8 | 9 | /// Returns the animation duration for a state transition of this view controller. 10 | /// 11 | /// - Parameter isAppearing: Whether this view controller is appearing. 12 | /// - Returns: A transition duration. 13 | func stateTransitionDuration(isAppearing: Bool) -> TimeInterval 14 | 15 | /// Notifies that a state transition will begin for this view controller. 16 | /// 17 | /// - Parameter isAppearing: Whether this view controller is appearing. 18 | func stateTransitionWillBegin(isAppearing: Bool) 19 | 20 | /// Notifies that a state transition did end for this view controller. 21 | /// 22 | /// - Parameter isAppearing: Whether this view controller is appearing. 23 | func stateTransitionDidEnd(isAppearing: Bool) 24 | 25 | /// Animations performed alongside the state transition of this view controller. 26 | /// 27 | /// - Parameter isAppearing: Whether this view controller is appearing. 28 | func animateAlongsideStateTransition(isAppearing: Bool) 29 | 30 | /// Returns the animation delay for a state transition of the provided view controller. 31 | /// 32 | /// - Parameter isAppearing: Whether this view controller is appearing. 33 | /// - Returns: A transition duration. 34 | func stateTransitionDelay(isAppearing: Bool) -> TimeInterval 35 | } 36 | 37 | #endif 38 | -------------------------------------------------------------------------------- /StateViewController.xcodeproj/StateViewController_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundleDevelopmentRegion 5 | en 6 | CFBundleExecutable 7 | $(EXECUTABLE_NAME) 8 | CFBundleIdentifier 9 | $(PRODUCT_BUNDLE_IDENTIFIER) 10 | CFBundleInfoDictionaryVersion 11 | 6.0 12 | CFBundleName 13 | $(PRODUCT_NAME) 14 | CFBundlePackageType 15 | FMWK 16 | CFBundleShortVersionString 17 | 1.0 18 | CFBundleSignature 19 | ???? 20 | CFBundleVersion 21 | $(CURRENT_PROJECT_VERSION) 22 | NSPrincipalClass 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /StateViewController.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = "1"; 4 | objectVersion = "46"; 5 | objects = { 6 | "OBJ_1" = { 7 | isa = "PBXProject"; 8 | attributes = { 9 | LastSwiftMigration = "9999"; 10 | LastUpgradeCheck = "9999"; 11 | }; 12 | buildConfigurationList = "OBJ_2"; 13 | compatibilityVersion = "Xcode 3.2"; 14 | developmentRegion = "en"; 15 | hasScannedForEncodings = "0"; 16 | knownRegions = ( 17 | "en" 18 | ); 19 | mainGroup = "OBJ_5"; 20 | productRefGroup = "OBJ_12"; 21 | projectDirPath = "."; 22 | targets = ( 23 | "StateViewController::StateViewController", 24 | "StateViewController::SwiftPMPackageDescription" 25 | ); 26 | }; 27 | "OBJ_10" = { 28 | isa = "PBXFileReference"; 29 | path = "StateViewControllerTransitioning.swift"; 30 | sourceTree = ""; 31 | }; 32 | "OBJ_11" = { 33 | isa = "PBXGroup"; 34 | children = ( 35 | ); 36 | name = "Tests"; 37 | path = ""; 38 | sourceTree = "SOURCE_ROOT"; 39 | }; 40 | "OBJ_12" = { 41 | isa = "PBXGroup"; 42 | children = ( 43 | "StateViewController::StateViewController::Product" 44 | ); 45 | name = "Products"; 46 | path = ""; 47 | sourceTree = "BUILT_PRODUCTS_DIR"; 48 | }; 49 | "OBJ_14" = { 50 | isa = "PBXFileReference"; 51 | path = "images"; 52 | sourceTree = "SOURCE_ROOT"; 53 | }; 54 | "OBJ_15" = { 55 | isa = "PBXFileReference"; 56 | path = "StateViewControllerExample"; 57 | sourceTree = "SOURCE_ROOT"; 58 | }; 59 | "OBJ_16" = { 60 | isa = "PBXFileReference"; 61 | path = "Example"; 62 | sourceTree = "SOURCE_ROOT"; 63 | }; 64 | "OBJ_17" = { 65 | isa = "PBXFileReference"; 66 | path = "Carthage"; 67 | sourceTree = "SOURCE_ROOT"; 68 | }; 69 | "OBJ_18" = { 70 | isa = "PBXFileReference"; 71 | path = "docs"; 72 | sourceTree = "SOURCE_ROOT"; 73 | }; 74 | "OBJ_19" = { 75 | isa = "PBXFileReference"; 76 | path = "build"; 77 | sourceTree = "SOURCE_ROOT"; 78 | }; 79 | "OBJ_2" = { 80 | isa = "XCConfigurationList"; 81 | buildConfigurations = ( 82 | "OBJ_3", 83 | "OBJ_4" 84 | ); 85 | defaultConfigurationIsVisible = "0"; 86 | defaultConfigurationName = "Release"; 87 | }; 88 | "OBJ_20" = { 89 | isa = "PBXFileReference"; 90 | path = "LICENSE"; 91 | sourceTree = ""; 92 | }; 93 | "OBJ_21" = { 94 | isa = "PBXFileReference"; 95 | path = "README.md"; 96 | sourceTree = ""; 97 | }; 98 | "OBJ_23" = { 99 | isa = "XCConfigurationList"; 100 | buildConfigurations = ( 101 | "OBJ_24", 102 | "OBJ_25" 103 | ); 104 | defaultConfigurationIsVisible = "0"; 105 | defaultConfigurationName = "Release"; 106 | }; 107 | "OBJ_24" = { 108 | isa = "XCBuildConfiguration"; 109 | buildSettings = { 110 | ENABLE_TESTABILITY = "YES"; 111 | FRAMEWORK_SEARCH_PATHS = ( 112 | "$(inherited)", 113 | "$(PLATFORM_DIR)/Developer/Library/Frameworks" 114 | ); 115 | HEADER_SEARCH_PATHS = ( 116 | "$(inherited)" 117 | ); 118 | INFOPLIST_FILE = "StateViewController.xcodeproj/StateViewController_Info.plist"; 119 | IPHONEOS_DEPLOYMENT_TARGET = "8.0"; 120 | LD_RUNPATH_SEARCH_PATHS = ( 121 | "$(inherited)", 122 | "$(TOOLCHAIN_DIR)/usr/lib/swift/macosx" 123 | ); 124 | MACOSX_DEPLOYMENT_TARGET = "10.10"; 125 | OTHER_CFLAGS = ( 126 | "$(inherited)" 127 | ); 128 | OTHER_LDFLAGS = ( 129 | "$(inherited)" 130 | ); 131 | OTHER_SWIFT_FLAGS = ( 132 | "$(inherited)" 133 | ); 134 | PRODUCT_BUNDLE_IDENTIFIER = "StateViewController"; 135 | PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)"; 136 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 137 | SKIP_INSTALL = "YES"; 138 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = ( 139 | "$(inherited)" 140 | ); 141 | SWIFT_VERSION = "5.0"; 142 | TARGET_NAME = "StateViewController"; 143 | TVOS_DEPLOYMENT_TARGET = "9.0"; 144 | WATCHOS_DEPLOYMENT_TARGET = "2.0"; 145 | }; 146 | name = "Debug"; 147 | }; 148 | "OBJ_25" = { 149 | isa = "XCBuildConfiguration"; 150 | buildSettings = { 151 | ENABLE_TESTABILITY = "YES"; 152 | FRAMEWORK_SEARCH_PATHS = ( 153 | "$(inherited)", 154 | "$(PLATFORM_DIR)/Developer/Library/Frameworks" 155 | ); 156 | HEADER_SEARCH_PATHS = ( 157 | "$(inherited)" 158 | ); 159 | INFOPLIST_FILE = "StateViewController.xcodeproj/StateViewController_Info.plist"; 160 | IPHONEOS_DEPLOYMENT_TARGET = "8.0"; 161 | LD_RUNPATH_SEARCH_PATHS = ( 162 | "$(inherited)", 163 | "$(TOOLCHAIN_DIR)/usr/lib/swift/macosx" 164 | ); 165 | MACOSX_DEPLOYMENT_TARGET = "10.10"; 166 | OTHER_CFLAGS = ( 167 | "$(inherited)" 168 | ); 169 | OTHER_LDFLAGS = ( 170 | "$(inherited)" 171 | ); 172 | OTHER_SWIFT_FLAGS = ( 173 | "$(inherited)" 174 | ); 175 | PRODUCT_BUNDLE_IDENTIFIER = "StateViewController"; 176 | PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)"; 177 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 178 | SKIP_INSTALL = "YES"; 179 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = ( 180 | "$(inherited)" 181 | ); 182 | SWIFT_VERSION = "5.0"; 183 | TARGET_NAME = "StateViewController"; 184 | TVOS_DEPLOYMENT_TARGET = "9.0"; 185 | WATCHOS_DEPLOYMENT_TARGET = "2.0"; 186 | }; 187 | name = "Release"; 188 | }; 189 | "OBJ_26" = { 190 | isa = "PBXSourcesBuildPhase"; 191 | files = ( 192 | "OBJ_27", 193 | "OBJ_28" 194 | ); 195 | }; 196 | "OBJ_27" = { 197 | isa = "PBXBuildFile"; 198 | fileRef = "OBJ_9"; 199 | }; 200 | "OBJ_28" = { 201 | isa = "PBXBuildFile"; 202 | fileRef = "OBJ_10"; 203 | }; 204 | "OBJ_29" = { 205 | isa = "PBXFrameworksBuildPhase"; 206 | files = ( 207 | ); 208 | }; 209 | "OBJ_3" = { 210 | isa = "XCBuildConfiguration"; 211 | buildSettings = { 212 | CLANG_ENABLE_OBJC_ARC = "YES"; 213 | COMBINE_HIDPI_IMAGES = "YES"; 214 | COPY_PHASE_STRIP = "NO"; 215 | DEBUG_INFORMATION_FORMAT = "dwarf"; 216 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 217 | ENABLE_NS_ASSERTIONS = "YES"; 218 | GCC_OPTIMIZATION_LEVEL = "0"; 219 | GCC_PREPROCESSOR_DEFINITIONS = ( 220 | "$(inherited)", 221 | "SWIFT_PACKAGE=1", 222 | "DEBUG=1" 223 | ); 224 | MACOSX_DEPLOYMENT_TARGET = "10.10"; 225 | ONLY_ACTIVE_ARCH = "YES"; 226 | OTHER_SWIFT_FLAGS = ( 227 | "$(inherited)", 228 | "-DXcode" 229 | ); 230 | PRODUCT_NAME = "$(TARGET_NAME)"; 231 | SDKROOT = "macosx"; 232 | SUPPORTED_PLATFORMS = ( 233 | "macosx", 234 | "iphoneos", 235 | "iphonesimulator", 236 | "appletvos", 237 | "appletvsimulator", 238 | "watchos", 239 | "watchsimulator" 240 | ); 241 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = ( 242 | "$(inherited)", 243 | "SWIFT_PACKAGE", 244 | "DEBUG" 245 | ); 246 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 247 | USE_HEADERMAP = "NO"; 248 | }; 249 | name = "Debug"; 250 | }; 251 | "OBJ_31" = { 252 | isa = "XCConfigurationList"; 253 | buildConfigurations = ( 254 | "OBJ_32", 255 | "OBJ_33" 256 | ); 257 | defaultConfigurationIsVisible = "0"; 258 | defaultConfigurationName = "Release"; 259 | }; 260 | "OBJ_32" = { 261 | isa = "XCBuildConfiguration"; 262 | buildSettings = { 263 | LD = "/usr/bin/true"; 264 | OTHER_SWIFT_FLAGS = ( 265 | "-swift-version", 266 | "5", 267 | "-I", 268 | "$(TOOLCHAIN_DIR)/usr/lib/swift/pm/4_2", 269 | "-target", 270 | "x86_64-apple-macosx10.10", 271 | "-sdk", 272 | "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk", 273 | "-package-description-version", 274 | "5.2.0" 275 | ); 276 | SWIFT_VERSION = "5.0"; 277 | }; 278 | name = "Debug"; 279 | }; 280 | "OBJ_33" = { 281 | isa = "XCBuildConfiguration"; 282 | buildSettings = { 283 | LD = "/usr/bin/true"; 284 | OTHER_SWIFT_FLAGS = ( 285 | "-swift-version", 286 | "5", 287 | "-I", 288 | "$(TOOLCHAIN_DIR)/usr/lib/swift/pm/4_2", 289 | "-target", 290 | "x86_64-apple-macosx10.10", 291 | "-sdk", 292 | "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk", 293 | "-package-description-version", 294 | "5.2.0" 295 | ); 296 | SWIFT_VERSION = "5.0"; 297 | }; 298 | name = "Release"; 299 | }; 300 | "OBJ_34" = { 301 | isa = "PBXSourcesBuildPhase"; 302 | files = ( 303 | "OBJ_35" 304 | ); 305 | }; 306 | "OBJ_35" = { 307 | isa = "PBXBuildFile"; 308 | fileRef = "OBJ_6"; 309 | }; 310 | "OBJ_4" = { 311 | isa = "XCBuildConfiguration"; 312 | buildSettings = { 313 | CLANG_ENABLE_OBJC_ARC = "YES"; 314 | COMBINE_HIDPI_IMAGES = "YES"; 315 | COPY_PHASE_STRIP = "YES"; 316 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 317 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 318 | GCC_OPTIMIZATION_LEVEL = "s"; 319 | GCC_PREPROCESSOR_DEFINITIONS = ( 320 | "$(inherited)", 321 | "SWIFT_PACKAGE=1" 322 | ); 323 | MACOSX_DEPLOYMENT_TARGET = "10.10"; 324 | OTHER_SWIFT_FLAGS = ( 325 | "$(inherited)", 326 | "-DXcode" 327 | ); 328 | PRODUCT_NAME = "$(TARGET_NAME)"; 329 | SDKROOT = "macosx"; 330 | SUPPORTED_PLATFORMS = ( 331 | "macosx", 332 | "iphoneos", 333 | "iphonesimulator", 334 | "appletvos", 335 | "appletvsimulator", 336 | "watchos", 337 | "watchsimulator" 338 | ); 339 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = ( 340 | "$(inherited)", 341 | "SWIFT_PACKAGE" 342 | ); 343 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 344 | USE_HEADERMAP = "NO"; 345 | }; 346 | name = "Release"; 347 | }; 348 | "OBJ_5" = { 349 | isa = "PBXGroup"; 350 | children = ( 351 | "OBJ_6", 352 | "OBJ_7", 353 | "OBJ_11", 354 | "OBJ_12", 355 | "OBJ_14", 356 | "OBJ_15", 357 | "OBJ_16", 358 | "OBJ_17", 359 | "OBJ_18", 360 | "OBJ_19", 361 | "OBJ_20", 362 | "OBJ_21" 363 | ); 364 | path = ""; 365 | sourceTree = ""; 366 | }; 367 | "OBJ_6" = { 368 | isa = "PBXFileReference"; 369 | explicitFileType = "sourcecode.swift"; 370 | path = "Package.swift"; 371 | sourceTree = ""; 372 | }; 373 | "OBJ_7" = { 374 | isa = "PBXGroup"; 375 | children = ( 376 | "OBJ_8" 377 | ); 378 | name = "Sources"; 379 | path = ""; 380 | sourceTree = "SOURCE_ROOT"; 381 | }; 382 | "OBJ_8" = { 383 | isa = "PBXGroup"; 384 | children = ( 385 | "OBJ_9", 386 | "OBJ_10" 387 | ); 388 | name = "StateViewController"; 389 | path = "Sources/StateViewController"; 390 | sourceTree = "SOURCE_ROOT"; 391 | }; 392 | "OBJ_9" = { 393 | isa = "PBXFileReference"; 394 | path = "StateViewController.swift"; 395 | sourceTree = ""; 396 | }; 397 | "StateViewController::StateViewController" = { 398 | isa = "PBXNativeTarget"; 399 | buildConfigurationList = "OBJ_23"; 400 | buildPhases = ( 401 | "OBJ_26", 402 | "OBJ_29" 403 | ); 404 | dependencies = ( 405 | ); 406 | name = "StateViewController"; 407 | productName = "StateViewController"; 408 | productReference = "StateViewController::StateViewController::Product"; 409 | productType = "com.apple.product-type.framework"; 410 | }; 411 | "StateViewController::StateViewController::Product" = { 412 | isa = "PBXFileReference"; 413 | path = "StateViewController.framework"; 414 | sourceTree = "BUILT_PRODUCTS_DIR"; 415 | }; 416 | "StateViewController::SwiftPMPackageDescription" = { 417 | isa = "PBXNativeTarget"; 418 | buildConfigurationList = "OBJ_31"; 419 | buildPhases = ( 420 | "OBJ_34" 421 | ); 422 | dependencies = ( 423 | ); 424 | name = "StateViewControllerPackageDescription"; 425 | productName = "StateViewControllerPackageDescription"; 426 | productType = "com.apple.product-type.framework"; 427 | }; 428 | }; 429 | rootObject = "OBJ_1"; 430 | } 431 | -------------------------------------------------------------------------------- /StateViewController.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /StateViewController.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /StateViewController.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded 6 | 7 | 8 | -------------------------------------------------------------------------------- /StateViewController.xcodeproj/xcshareddata/xcschemes/StateViewController-Package.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 53 | 54 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /docs/Classes.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Classes Reference 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 |

23 | 24 | Docs 25 | 26 | (0% documented) 27 |

28 | 29 |

30 |

31 | 32 |
33 |

34 | 35 |

36 | 37 | 38 | View on GitHub 39 | 40 |

41 | 42 |
43 | 44 | 49 | 50 |
51 | 71 |
72 | 73 |
74 |
75 |

Classes

76 |

The following classes are available globally.

77 | 78 |
79 |
80 | 81 |
82 |
83 |
84 |
    85 |
  • 86 |
    87 | 88 | 89 | 90 | StateViewController 91 | 92 |
    93 |
    94 |
    95 |
    96 |
    97 |
    98 |

    A container view controller that manages the appearance of one or more child view controller for any given state.

    99 |

    Overview

    100 | 101 |

    This class is designed to make stateful view controller programming easier. Typically in iOS development, 102 | views representing multiple states are managed in one single view controller, leading to large view controller 103 | classes that quickly become hard to work with and overlook at a glance. For instance, a view controller may 104 | display an activity indicator while a network call is performed, leaving the view controller to have to directly 105 | manipulate view hierarhy for each state. Furthermore, the state of a view controller tends to be represented by 106 | conditions that are hard to synchronize, easily becoming a source of bugs and unexpected behavior. 107 | With StateViewController each state can be represented by one or more view controllers. 108 | This allows you to composite view controllers in self-contained classes resulting in smaller view 109 | controllers and better ability modularize your view controller code, with clear separation between states.

    110 |

    Subclassing notes

    111 | 112 |

    You must subclass StateViewController and define a state for the view controller you are creating.

    113 |
    enum MyViewControllerState {
    114 |     case loading
    115 |     case ready
    116 | }
    117 | 
    118 | 119 |

    Note: Your state must conform to Equatable in order for StateViewController to distinguish between states.

    120 | 121 |

    Override loadAppearanceState() to determine which state is being represented each time this view controller 122 | is appearing on screen. In this method is appropriate to query your model layer to determine whether data needed 123 | for a certain state is available or not.

    124 |
    override func loadAppearanceState() -> MyViewControllerState {
    125 |     if model.isDataAvailable {
    126 |         return .ready
    127 |     } else {
    128 |         return .loading
    129 |     }
    130 | }
    131 | 
    132 | 133 |

    To determine which content view controllers represent a particular state, you must override 134 | children(for:).

    135 |
    override func children(for state: MyViewControllerState) -> [UIViewController] {
    136 |     switch state {
    137 |     case .loading:
    138 |         return [ActivityIndicatorViewController()]
    139 |     case .empty:
    140 |         return [myChild]
    141 |     }
    142 | }
    143 | 
    144 | 145 |

    Callback methods are overridable, notifying you when a state transition is being performed, and what child 146 | view controllers are being presented as a result of a state transition.

    147 | 148 |

    Using willTransition(to:animated:) you should prepare view controller representing the state being transition to 149 | with the appropriate data.

    150 |
    override func willTransition(to state: MyViewControllerState, animated: Bool) {
    151 |     switch state {
    152 |     case .ready:
    153 |         myChild.content = myLoadedContent
    154 |     case .loading:
    155 |         break
    156 |     }
    157 | }
    158 | 
    159 | 160 |

    Overriding didTransition(to:animated:) is an appropriate place to invoke methods that eventually results in 161 | a state transition being requested using setNeedsTransition(to:animated:), as it ensures that any previous state 162 | transitions has been fully completed.

    163 |
    override func didTransition(from previousState: MyViewControllerState?, animated: Bool) {
    164 |     switch state {
    165 |     case .ready:
    166 |         break
    167 |     case .loading:
    168 |         model.loadData { result in
    169 |             self.myLoadedContent = result
    170 |             self.setNeedsTransition(to: .ready, animated: true)
    171 |         }
    172 |     }
    173 | }
    174 | 
    175 | 176 |

    You may also override loadChildContainerView() to provide a custom container view for your 177 | content view controllers, allowing you to manipulate the view hierarchy above and below the content view 178 | controller container view.

    179 |

    Animating state transitions

    180 | 181 |

    By default, no animations are performed between states. To enable animations, you have three options:

    182 | 183 |
      184 |
    • Set defaultStateTransitioningCoordinator
    • 185 |
    • Override stateTransitionCoordinator(for:) in your StateViewController subclasses
    • 186 |
    • Conform view controllers contained in StateViewController to StateViewControllerTransitioning.
    • 187 |
    188 | 189 | See more 190 |
    191 |
    192 |
    193 |
  • 194 |
195 |
196 |
197 |
198 | 199 |
200 |
201 | 205 | 206 | 207 | 208 | -------------------------------------------------------------------------------- /docs/Protocols.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Protocols Reference 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 |

23 | 24 | Docs 25 | 26 | (0% documented) 27 |

28 | 29 |

30 |

31 | 32 |
33 |

34 | 35 |

36 | 37 | 38 | View on GitHub 39 | 40 |

41 | 42 |
43 | 44 | 49 | 50 |
51 | 71 |
72 | 73 |
74 |
75 |

Protocols

76 |

The following protocols are available globally.

77 | 78 |
79 |
80 | 81 |
82 |
83 |
84 |
    85 |
  • 86 |
    87 | 88 | 89 | 90 | StateViewControllerTransitioning 91 | 92 |
    93 |
    94 |
    95 |
    96 |
    97 |
    98 |

    View controllers can conform to this protocol to provide their desired 99 | state transitioning behaviour when contained in a StateViewController.

    100 | 101 | See more 102 |
    103 |
    104 |
    105 |
  • 106 |
107 |
108 |
109 |
110 | 111 |
112 |
113 | 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /docs/Protocols/StateViewControllerTransitioning.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | StateViewControllerTransitioning Protocol Reference 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 |

23 | 24 | Docs 25 | 26 | (0% documented) 27 |

28 | 29 |

30 |

31 | 32 |
33 |

34 | 35 |

36 | 37 | 38 | View on GitHub 39 | 40 |

41 | 42 |
43 | 44 | 49 | 50 |
51 | 71 |
72 | 73 |
74 |
75 |

StateViewControllerTransitioning

76 |

View controllers can conform to this protocol to provide their desired 77 | state transitioning behaviour when contained in a StateViewController.

78 | 79 |
80 |
81 | 82 |
83 |
84 |
85 |
    86 |
  • 87 |
    88 | 89 | 90 | 91 | stateTransitionDuration(isAppearing:) 92 | 93 |
    94 |
    95 |
    96 |
    97 |
    98 |
    99 |

    Returns the animation duration for a state transition of this view controller.

    100 | 101 |
    102 |
    103 |
    104 |
  • 105 |
  • 106 |
    107 | 108 | 109 | 110 | stateTransitionWillBegin(isAppearing:) 111 | 112 |
    113 |
    114 |
    115 |
    116 |
    117 |
    118 |

    Notifies that a state transition will begin for this view controller.

    119 | 120 |
    121 |
    122 |
    123 |
  • 124 |
  • 125 |
    126 | 127 | 128 | 129 | stateTransitionDidEnd(isAppearing:) 130 | 131 |
    132 |
    133 |
    134 |
    135 |
    136 |
    137 |

    Notifies that a state transition did end for this view controller.

    138 | 139 |
    140 |
    141 |
    142 |
  • 143 |
  • 144 |
    145 | 146 | 147 | 148 | animateAlongsideStateTransition(isAppearing:) 149 | 150 |
    151 |
    152 |
    153 |
    154 |
    155 |
    156 |

    Animations performed alongside the state transition of this view controller.

    157 | 158 |
    159 |
    160 |
    161 |
  • 162 |
  • 163 |
    164 | 165 | 166 | 167 | stateTransitionDelay(isAppearing:) 168 | 169 |
    170 |
    171 |
    172 |
    173 |
    174 |
    175 |

    Returns the animation delay for a state transition of the provided view controller.

    176 | 177 |
    178 |
    179 |
    180 |
  • 181 |
182 |
183 |
184 |
185 | 186 |
187 |
188 | 192 | 193 | 194 | 195 | -------------------------------------------------------------------------------- /docs/badge.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | documentation 17 | 18 | 19 | documentation 20 | 21 | 22 | 0% 23 | 24 | 25 | 0% 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /docs/css/highlight.css: -------------------------------------------------------------------------------- 1 | /* Credit to https://gist.github.com/wataru420/2048287 */ 2 | .highlight { 3 | /* Comment */ 4 | /* Error */ 5 | /* Keyword */ 6 | /* Operator */ 7 | /* Comment.Multiline */ 8 | /* Comment.Preproc */ 9 | /* Comment.Single */ 10 | /* Comment.Special */ 11 | /* Generic.Deleted */ 12 | /* Generic.Deleted.Specific */ 13 | /* Generic.Emph */ 14 | /* Generic.Error */ 15 | /* Generic.Heading */ 16 | /* Generic.Inserted */ 17 | /* Generic.Inserted.Specific */ 18 | /* Generic.Output */ 19 | /* Generic.Prompt */ 20 | /* Generic.Strong */ 21 | /* Generic.Subheading */ 22 | /* Generic.Traceback */ 23 | /* Keyword.Constant */ 24 | /* Keyword.Declaration */ 25 | /* Keyword.Pseudo */ 26 | /* Keyword.Reserved */ 27 | /* Keyword.Type */ 28 | /* Literal.Number */ 29 | /* Literal.String */ 30 | /* Name.Attribute */ 31 | /* Name.Builtin */ 32 | /* Name.Class */ 33 | /* Name.Constant */ 34 | /* Name.Entity */ 35 | /* Name.Exception */ 36 | /* Name.Function */ 37 | /* Name.Namespace */ 38 | /* Name.Tag */ 39 | /* Name.Variable */ 40 | /* Operator.Word */ 41 | /* Text.Whitespace */ 42 | /* Literal.Number.Float */ 43 | /* Literal.Number.Hex */ 44 | /* Literal.Number.Integer */ 45 | /* Literal.Number.Oct */ 46 | /* Literal.String.Backtick */ 47 | /* Literal.String.Char */ 48 | /* Literal.String.Doc */ 49 | /* Literal.String.Double */ 50 | /* Literal.String.Escape */ 51 | /* Literal.String.Heredoc */ 52 | /* Literal.String.Interpol */ 53 | /* Literal.String.Other */ 54 | /* Literal.String.Regex */ 55 | /* Literal.String.Single */ 56 | /* Literal.String.Symbol */ 57 | /* Name.Builtin.Pseudo */ 58 | /* Name.Variable.Class */ 59 | /* Name.Variable.Global */ 60 | /* Name.Variable.Instance */ 61 | /* Literal.Number.Integer.Long */ } 62 | .highlight .c { 63 | color: #999988; 64 | font-style: italic; } 65 | .highlight .err { 66 | color: #a61717; 67 | background-color: #e3d2d2; } 68 | .highlight .k { 69 | color: #000000; 70 | font-weight: bold; } 71 | .highlight .o { 72 | color: #000000; 73 | font-weight: bold; } 74 | .highlight .cm { 75 | color: #999988; 76 | font-style: italic; } 77 | .highlight .cp { 78 | color: #999999; 79 | font-weight: bold; } 80 | .highlight .c1 { 81 | color: #999988; 82 | font-style: italic; } 83 | .highlight .cs { 84 | color: #999999; 85 | font-weight: bold; 86 | font-style: italic; } 87 | .highlight .gd { 88 | color: #000000; 89 | background-color: #ffdddd; } 90 | .highlight .gd .x { 91 | color: #000000; 92 | background-color: #ffaaaa; } 93 | .highlight .ge { 94 | color: #000000; 95 | font-style: italic; } 96 | .highlight .gr { 97 | color: #aa0000; } 98 | .highlight .gh { 99 | color: #999999; } 100 | .highlight .gi { 101 | color: #000000; 102 | background-color: #ddffdd; } 103 | .highlight .gi .x { 104 | color: #000000; 105 | background-color: #aaffaa; } 106 | .highlight .go { 107 | color: #888888; } 108 | .highlight .gp { 109 | color: #555555; } 110 | .highlight .gs { 111 | font-weight: bold; } 112 | .highlight .gu { 113 | color: #aaaaaa; } 114 | .highlight .gt { 115 | color: #aa0000; } 116 | .highlight .kc { 117 | color: #000000; 118 | font-weight: bold; } 119 | .highlight .kd { 120 | color: #000000; 121 | font-weight: bold; } 122 | .highlight .kp { 123 | color: #000000; 124 | font-weight: bold; } 125 | .highlight .kr { 126 | color: #000000; 127 | font-weight: bold; } 128 | .highlight .kt { 129 | color: #445588; } 130 | .highlight .m { 131 | color: #009999; } 132 | .highlight .s { 133 | color: #d14; } 134 | .highlight .na { 135 | color: #008080; } 136 | .highlight .nb { 137 | color: #0086B3; } 138 | .highlight .nc { 139 | color: #445588; 140 | font-weight: bold; } 141 | .highlight .no { 142 | color: #008080; } 143 | .highlight .ni { 144 | color: #800080; } 145 | .highlight .ne { 146 | color: #990000; 147 | font-weight: bold; } 148 | .highlight .nf { 149 | color: #990000; } 150 | .highlight .nn { 151 | color: #555555; } 152 | .highlight .nt { 153 | color: #000080; } 154 | .highlight .nv { 155 | color: #008080; } 156 | .highlight .ow { 157 | color: #000000; 158 | font-weight: bold; } 159 | .highlight .w { 160 | color: #bbbbbb; } 161 | .highlight .mf { 162 | color: #009999; } 163 | .highlight .mh { 164 | color: #009999; } 165 | .highlight .mi { 166 | color: #009999; } 167 | .highlight .mo { 168 | color: #009999; } 169 | .highlight .sb { 170 | color: #d14; } 171 | .highlight .sc { 172 | color: #d14; } 173 | .highlight .sd { 174 | color: #d14; } 175 | .highlight .s2 { 176 | color: #d14; } 177 | .highlight .se { 178 | color: #d14; } 179 | .highlight .sh { 180 | color: #d14; } 181 | .highlight .si { 182 | color: #d14; } 183 | .highlight .sx { 184 | color: #d14; } 185 | .highlight .sr { 186 | color: #009926; } 187 | .highlight .s1 { 188 | color: #d14; } 189 | .highlight .ss { 190 | color: #990073; } 191 | .highlight .bp { 192 | color: #999999; } 193 | .highlight .vc { 194 | color: #008080; } 195 | .highlight .vg { 196 | color: #008080; } 197 | .highlight .vi { 198 | color: #008080; } 199 | .highlight .il { 200 | color: #009999; } 201 | -------------------------------------------------------------------------------- /docs/css/jazzy.css: -------------------------------------------------------------------------------- 1 | *, *:before, *:after { 2 | box-sizing: inherit; } 3 | 4 | body { 5 | margin: 0; 6 | background: #fff; 7 | color: #333; 8 | font: 16px/1.7 "Helvetica Neue", Helvetica, Arial, sans-serif; 9 | letter-spacing: .2px; 10 | -webkit-font-smoothing: antialiased; 11 | box-sizing: border-box; } 12 | 13 | h1 { 14 | font-size: 2rem; 15 | font-weight: 700; 16 | margin: 1.275em 0 0.6em; } 17 | 18 | h2 { 19 | font-size: 1.75rem; 20 | font-weight: 700; 21 | margin: 1.275em 0 0.3em; } 22 | 23 | h3 { 24 | font-size: 1.5rem; 25 | font-weight: 700; 26 | margin: 1em 0 0.3em; } 27 | 28 | h4 { 29 | font-size: 1.25rem; 30 | font-weight: 700; 31 | margin: 1.275em 0 0.85em; } 32 | 33 | h5 { 34 | font-size: 1rem; 35 | font-weight: 700; 36 | margin: 1.275em 0 0.85em; } 37 | 38 | h6 { 39 | font-size: 1rem; 40 | font-weight: 700; 41 | margin: 1.275em 0 0.85em; 42 | color: #777; } 43 | 44 | p { 45 | margin: 0 0 1em; } 46 | 47 | ul, ol { 48 | padding: 0 0 0 2em; 49 | margin: 0 0 0.85em; } 50 | 51 | blockquote { 52 | margin: 0 0 0.85em; 53 | padding: 0 15px; 54 | color: #858585; 55 | border-left: 4px solid #e5e5e5; } 56 | 57 | img { 58 | max-width: 100%; } 59 | 60 | a { 61 | color: #4183c4; 62 | text-decoration: none; } 63 | a:hover, a:focus { 64 | outline: 0; 65 | text-decoration: underline; } 66 | a.discouraged { 67 | text-decoration: line-through; } 68 | a.discouraged:hover, a.discouraged:focus { 69 | text-decoration: underline line-through; } 70 | 71 | table { 72 | background: #fff; 73 | width: 100%; 74 | border-collapse: collapse; 75 | border-spacing: 0; 76 | overflow: auto; 77 | margin: 0 0 0.85em; } 78 | 79 | tr:nth-child(2n) { 80 | background-color: #fbfbfb; } 81 | 82 | th, td { 83 | padding: 6px 13px; 84 | border: 1px solid #ddd; } 85 | 86 | pre { 87 | margin: 0 0 1.275em; 88 | padding: .85em 1em; 89 | overflow: auto; 90 | background: #f7f7f7; 91 | font-size: .85em; 92 | font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; } 93 | 94 | code { 95 | font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; } 96 | 97 | .item-container p > code, .item-container li > code, .top-matter p > code, .top-matter li > code { 98 | background: #f7f7f7; 99 | padding: .2em; } 100 | .item-container p > code:before, .item-container p > code:after, .item-container li > code:before, .item-container li > code:after, .top-matter p > code:before, .top-matter p > code:after, .top-matter li > code:before, .top-matter li > code:after { 101 | letter-spacing: -.2em; 102 | content: "\00a0"; } 103 | 104 | pre code { 105 | padding: 0; 106 | white-space: pre; } 107 | 108 | .content-wrapper { 109 | display: flex; 110 | flex-direction: column; } 111 | @media (min-width: 768px) { 112 | .content-wrapper { 113 | flex-direction: row; } } 114 | 115 | .header { 116 | display: flex; 117 | padding: 8px; 118 | font-size: 0.875em; 119 | background: #444; 120 | color: #999; } 121 | 122 | .header-col { 123 | margin: 0; 124 | padding: 0 8px; } 125 | 126 | .header-col--primary { 127 | flex: 1; } 128 | 129 | .header-link { 130 | color: #fff; } 131 | 132 | .header-icon { 133 | padding-right: 6px; 134 | vertical-align: -4px; 135 | height: 16px; } 136 | 137 | .breadcrumbs { 138 | font-size: 0.875em; 139 | padding: 8px 16px; 140 | margin: 0; 141 | background: #fbfbfb; 142 | border-bottom: 1px solid #ddd; } 143 | 144 | .carat { 145 | height: 10px; 146 | margin: 0 5px; } 147 | 148 | .navigation { 149 | order: 2; } 150 | @media (min-width: 768px) { 151 | .navigation { 152 | order: 1; 153 | width: 25%; 154 | max-width: 300px; 155 | padding-bottom: 64px; 156 | overflow: hidden; 157 | word-wrap: normal; 158 | background: #fbfbfb; 159 | border-right: 1px solid #ddd; } } 160 | 161 | .nav-groups { 162 | list-style-type: none; 163 | padding-left: 0; } 164 | 165 | .nav-group-name { 166 | border-bottom: 1px solid #ddd; 167 | padding: 8px 0 8px 16px; } 168 | 169 | .nav-group-name-link { 170 | color: #333; } 171 | 172 | .nav-group-tasks { 173 | margin: 8px 0; 174 | padding: 0 0 0 8px; } 175 | 176 | .nav-group-task { 177 | font-size: 1em; 178 | list-style-type: none; 179 | white-space: nowrap; } 180 | 181 | .nav-group-task-link { 182 | color: #808080; } 183 | 184 | .main-content { 185 | order: 1; } 186 | @media (min-width: 768px) { 187 | .main-content { 188 | order: 2; 189 | flex: 1; 190 | padding-bottom: 60px; } } 191 | 192 | .section { 193 | padding: 0 32px; 194 | border-bottom: 1px solid #ddd; } 195 | 196 | .section-content { 197 | max-width: 834px; 198 | margin: 0 auto; 199 | padding: 16px 0; } 200 | 201 | .section-name { 202 | color: #666; 203 | display: block; } 204 | .section-name p { 205 | margin-bottom: inherit; } 206 | 207 | .declaration .highlight { 208 | overflow-x: initial; 209 | padding: 8px 0; 210 | margin: 0; 211 | background-color: transparent; 212 | border: none; } 213 | 214 | .task-group-section { 215 | border-top: 1px solid #ddd; } 216 | 217 | .task-group { 218 | padding-top: 0px; } 219 | 220 | .task-name-container a[name]:before { 221 | content: ""; 222 | display: block; } 223 | 224 | .section-name-container { 225 | position: relative; } 226 | .section-name-container .section-name-link { 227 | position: absolute; 228 | top: 0; 229 | left: 0; 230 | bottom: 0; 231 | right: 0; 232 | margin-bottom: 0; } 233 | .section-name-container .section-name { 234 | position: relative; 235 | pointer-events: none; 236 | z-index: 1; } 237 | .section-name-container .section-name a { 238 | pointer-events: auto; } 239 | 240 | .item-container { 241 | padding: 0; } 242 | 243 | .item { 244 | padding-top: 8px; 245 | width: 100%; 246 | list-style-type: none; } 247 | .item a[name]:before { 248 | content: ""; 249 | display: block; } 250 | .item .token, .item .direct-link { 251 | padding-left: 3px; 252 | margin-left: 0px; 253 | font-size: 1rem; } 254 | .item .declaration-note { 255 | font-size: .85em; 256 | color: #808080; 257 | font-style: italic; } 258 | 259 | .pointer-container { 260 | border-bottom: 1px solid #ddd; 261 | left: -23px; 262 | padding-bottom: 13px; 263 | position: relative; 264 | width: 110%; } 265 | 266 | .pointer { 267 | left: 21px; 268 | top: 7px; 269 | display: block; 270 | position: absolute; 271 | width: 12px; 272 | height: 12px; 273 | border-left: 1px solid #ddd; 274 | border-top: 1px solid #ddd; 275 | background: #fff; 276 | transform: rotate(45deg); } 277 | 278 | .height-container { 279 | display: none; 280 | position: relative; 281 | width: 100%; 282 | overflow: hidden; } 283 | .height-container .section { 284 | background: #fff; 285 | border: 1px solid #ddd; 286 | border-top-width: 0; 287 | padding-top: 10px; 288 | padding-bottom: 5px; 289 | padding: 8px 16px; } 290 | 291 | .aside, .language { 292 | padding: 6px 12px; 293 | margin: 12px 0; 294 | border-left: 5px solid #dddddd; 295 | overflow-y: hidden; } 296 | .aside .aside-title, .language .aside-title { 297 | font-size: 9px; 298 | letter-spacing: 2px; 299 | text-transform: uppercase; 300 | padding-bottom: 0; 301 | margin: 0; 302 | color: #aaa; 303 | -webkit-user-select: none; } 304 | .aside p:last-child, .language p:last-child { 305 | margin-bottom: 0; } 306 | 307 | .language { 308 | border-left: 5px solid #cde9f4; } 309 | .language .aside-title { 310 | color: #4183c4; } 311 | 312 | .aside-warning, .aside-deprecated, .aside-unavailable { 313 | border-left: 5px solid #ff6666; } 314 | .aside-warning .aside-title, .aside-deprecated .aside-title, .aside-unavailable .aside-title { 315 | color: #ff0000; } 316 | 317 | .graybox { 318 | border-collapse: collapse; 319 | width: 100%; } 320 | .graybox p { 321 | margin: 0; 322 | word-break: break-word; 323 | min-width: 50px; } 324 | .graybox td { 325 | border: 1px solid #ddd; 326 | padding: 5px 25px 5px 10px; 327 | vertical-align: middle; } 328 | .graybox tr td:first-of-type { 329 | text-align: right; 330 | padding: 7px; 331 | vertical-align: top; 332 | word-break: normal; 333 | width: 40px; } 334 | 335 | .slightly-smaller { 336 | font-size: 0.9em; } 337 | 338 | .footer { 339 | padding: 8px 16px; 340 | background: #444; 341 | color: #ddd; 342 | font-size: 0.8em; } 343 | .footer p { 344 | margin: 8px 0; } 345 | .footer a { 346 | color: #fff; } 347 | 348 | html.dash .header, html.dash .breadcrumbs, html.dash .navigation { 349 | display: none; } 350 | 351 | html.dash .height-container { 352 | display: block; } 353 | 354 | form[role=search] input { 355 | font: 16px/1.7 "Helvetica Neue", Helvetica, Arial, sans-serif; 356 | font-size: 14px; 357 | line-height: 24px; 358 | padding: 0 10px; 359 | margin: 0; 360 | border: none; 361 | border-radius: 1em; } 362 | .loading form[role=search] input { 363 | background: white url(../img/spinner.gif) center right 4px no-repeat; } 364 | 365 | form[role=search] .tt-menu { 366 | margin: 0; 367 | min-width: 300px; 368 | background: #fbfbfb; 369 | color: #333; 370 | border: 1px solid #ddd; } 371 | 372 | form[role=search] .tt-highlight { 373 | font-weight: bold; } 374 | 375 | form[role=search] .tt-suggestion { 376 | font: 16px/1.7 "Helvetica Neue", Helvetica, Arial, sans-serif; 377 | padding: 0 8px; } 378 | form[role=search] .tt-suggestion span { 379 | display: table-cell; 380 | white-space: nowrap; } 381 | form[role=search] .tt-suggestion .doc-parent-name { 382 | width: 100%; 383 | text-align: right; 384 | font-weight: normal; 385 | font-size: 0.9em; 386 | padding-left: 16px; } 387 | 388 | form[role=search] .tt-suggestion:hover, 389 | form[role=search] .tt-suggestion.tt-cursor { 390 | cursor: pointer; 391 | background-color: #4183c4; 392 | color: #fff; } 393 | 394 | form[role=search] .tt-suggestion:hover .doc-parent-name, 395 | form[role=search] .tt-suggestion.tt-cursor .doc-parent-name { 396 | color: #fff; } 397 | -------------------------------------------------------------------------------- /docs/docsets/.docset/Contents/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleIdentifier 6 | com.jazzy. 7 | CFBundleName 8 | 9 | DocSetPlatformFamily 10 | 11 | isDashDocset 12 | 13 | dashIndexFilePath 14 | index.html 15 | isJavaScriptEnabled 16 | 17 | DashDocSetFamily 18 | dashtoc 19 | 20 | 21 | -------------------------------------------------------------------------------- /docs/docsets/.docset/Contents/Resources/Documents/Classes.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Classes Reference 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 |

23 | 24 | Docs 25 | 26 | (0% documented) 27 |

28 | 29 |

30 |

31 | 32 |
33 |

34 | 35 |

36 | 37 | 38 | View on GitHub 39 | 40 |

41 | 42 |
43 | 44 | 49 | 50 |
51 | 71 |
72 | 73 |
74 |
75 |

Classes

76 |

The following classes are available globally.

77 | 78 |
79 |
80 | 81 |
82 |
83 |
84 |
    85 |
  • 86 |
    87 | 88 | 89 | 90 | StateViewController 91 | 92 |
    93 |
    94 |
    95 |
    96 |
    97 |
    98 |

    A container view controller that manages the appearance of one or more child view controller for any given state.

    99 |

    Overview

    100 | 101 |

    This class is designed to make stateful view controller programming easier. Typically in iOS development, 102 | views representing multiple states are managed in one single view controller, leading to large view controller 103 | classes that quickly become hard to work with and overlook at a glance. For instance, a view controller may 104 | display an activity indicator while a network call is performed, leaving the view controller to have to directly 105 | manipulate view hierarhy for each state. Furthermore, the state of a view controller tends to be represented by 106 | conditions that are hard to synchronize, easily becoming a source of bugs and unexpected behavior. 107 | With StateViewController each state can be represented by one or more view controllers. 108 | This allows you to composite view controllers in self-contained classes resulting in smaller view 109 | controllers and better ability modularize your view controller code, with clear separation between states.

    110 |

    Subclassing notes

    111 | 112 |

    You must subclass StateViewController and define a state for the view controller you are creating.

    113 |
    enum MyViewControllerState {
    114 |     case loading
    115 |     case ready
    116 | }
    117 | 
    118 | 119 |

    Note: Your state must conform to Equatable in order for StateViewController to distinguish between states.

    120 | 121 |

    Override loadAppearanceState() to determine which state is being represented each time this view controller 122 | is appearing on screen. In this method is appropriate to query your model layer to determine whether data needed 123 | for a certain state is available or not.

    124 |
    override func loadAppearanceState() -> MyViewControllerState {
    125 |     if model.isDataAvailable {
    126 |         return .ready
    127 |     } else {
    128 |         return .loading
    129 |     }
    130 | }
    131 | 
    132 | 133 |

    To determine which content view controllers represent a particular state, you must override 134 | children(for:).

    135 |
    override func children(for state: MyViewControllerState) -> [UIViewController] {
    136 |     switch state {
    137 |     case .loading:
    138 |         return [ActivityIndicatorViewController()]
    139 |     case .empty:
    140 |         return [myChild]
    141 |     }
    142 | }
    143 | 
    144 | 145 |

    Callback methods are overridable, notifying you when a state transition is being performed, and what child 146 | view controllers are being presented as a result of a state transition.

    147 | 148 |

    Using willTransition(to:animated:) you should prepare view controller representing the state being transition to 149 | with the appropriate data.

    150 |
    override func willTransition(to state: MyViewControllerState, animated: Bool) {
    151 |     switch state {
    152 |     case .ready:
    153 |         myChild.content = myLoadedContent
    154 |     case .loading:
    155 |         break
    156 |     }
    157 | }
    158 | 
    159 | 160 |

    Overriding didTransition(to:animated:) is an appropriate place to invoke methods that eventually results in 161 | a state transition being requested using setNeedsTransition(to:animated:), as it ensures that any previous state 162 | transitions has been fully completed.

    163 |
    override func didTransition(from previousState: MyViewControllerState?, animated: Bool) {
    164 |     switch state {
    165 |     case .ready:
    166 |         break
    167 |     case .loading:
    168 |         model.loadData { result in
    169 |             self.myLoadedContent = result
    170 |             self.setNeedsTransition(to: .ready, animated: true)
    171 |         }
    172 |     }
    173 | }
    174 | 
    175 | 176 |

    You may also override loadChildContainerView() to provide a custom container view for your 177 | content view controllers, allowing you to manipulate the view hierarchy above and below the content view 178 | controller container view.

    179 |

    Animating state transitions

    180 | 181 |

    By default, no animations are performed between states. To enable animations, you have three options:

    182 | 183 |
      184 |
    • Set defaultStateTransitioningCoordinator
    • 185 |
    • Override stateTransitionCoordinator(for:) in your StateViewController subclasses
    • 186 |
    • Conform view controllers contained in StateViewController to StateViewControllerTransitioning.
    • 187 |
    188 | 189 | See more 190 |
    191 |
    192 |
    193 |
  • 194 |
195 |
196 |
197 |
198 | 199 |
200 |
201 | 205 | 206 | 207 | 208 | -------------------------------------------------------------------------------- /docs/docsets/.docset/Contents/Resources/Documents/Protocols.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Protocols Reference 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 |

23 | 24 | Docs 25 | 26 | (0% documented) 27 |

28 | 29 |

30 |

31 | 32 |
33 |

34 | 35 |

36 | 37 | 38 | View on GitHub 39 | 40 |

41 | 42 |
43 | 44 | 49 | 50 |
51 | 71 |
72 | 73 |
74 |
75 |

Protocols

76 |

The following protocols are available globally.

77 | 78 |
79 |
80 | 81 |
82 |
83 |
84 |
    85 |
  • 86 |
    87 | 88 | 89 | 90 | StateViewControllerTransitioning 91 | 92 |
    93 |
    94 |
    95 |
    96 |
    97 |
    98 |

    View controllers can conform to this protocol to provide their desired 99 | state transitioning behaviour when contained in a StateViewController.

    100 | 101 | See more 102 |
    103 |
    104 |
    105 |
  • 106 |
107 |
108 |
109 |
110 | 111 |
112 |
113 | 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /docs/docsets/.docset/Contents/Resources/Documents/Protocols/StateViewControllerTransitioning.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | StateViewControllerTransitioning Protocol Reference 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 |

23 | 24 | Docs 25 | 26 | (0% documented) 27 |

28 | 29 |

30 |

31 | 32 |
33 |

34 | 35 |

36 | 37 | 38 | View on GitHub 39 | 40 |

41 | 42 |
43 | 44 | 49 | 50 |
51 | 71 |
72 | 73 |
74 |
75 |

StateViewControllerTransitioning

76 |

View controllers can conform to this protocol to provide their desired 77 | state transitioning behaviour when contained in a StateViewController.

78 | 79 |
80 |
81 | 82 |
83 |
84 |
85 |
    86 |
  • 87 |
    88 | 89 | 90 | 91 | stateTransitionDuration(isAppearing:) 92 | 93 |
    94 |
    95 |
    96 |
    97 |
    98 |
    99 |

    Returns the animation duration for a state transition of this view controller.

    100 | 101 |
    102 |
    103 |
    104 |
  • 105 |
  • 106 |
    107 | 108 | 109 | 110 | stateTransitionWillBegin(isAppearing:) 111 | 112 |
    113 |
    114 |
    115 |
    116 |
    117 |
    118 |

    Notifies that a state transition will begin for this view controller.

    119 | 120 |
    121 |
    122 |
    123 |
  • 124 |
  • 125 |
    126 | 127 | 128 | 129 | stateTransitionDidEnd(isAppearing:) 130 | 131 |
    132 |
    133 |
    134 |
    135 |
    136 |
    137 |

    Notifies that a state transition did end for this view controller.

    138 | 139 |
    140 |
    141 |
    142 |
  • 143 |
  • 144 |
    145 | 146 | 147 | 148 | animateAlongsideStateTransition(isAppearing:) 149 | 150 |
    151 |
    152 |
    153 |
    154 |
    155 |
    156 |

    Animations performed alongside the state transition of this view controller.

    157 | 158 |
    159 |
    160 |
    161 |
  • 162 |
  • 163 |
    164 | 165 | 166 | 167 | stateTransitionDelay(isAppearing:) 168 | 169 |
    170 |
    171 |
    172 |
    173 |
    174 |
    175 |

    Returns the animation delay for a state transition of the provided view controller.

    176 | 177 |
    178 |
    179 |
    180 |
  • 181 |
182 |
183 |
184 |
185 | 186 |
187 |
188 | 192 | 193 | 194 | 195 | -------------------------------------------------------------------------------- /docs/docsets/.docset/Contents/Resources/Documents/css/highlight.css: -------------------------------------------------------------------------------- 1 | /* Credit to https://gist.github.com/wataru420/2048287 */ 2 | .highlight { 3 | /* Comment */ 4 | /* Error */ 5 | /* Keyword */ 6 | /* Operator */ 7 | /* Comment.Multiline */ 8 | /* Comment.Preproc */ 9 | /* Comment.Single */ 10 | /* Comment.Special */ 11 | /* Generic.Deleted */ 12 | /* Generic.Deleted.Specific */ 13 | /* Generic.Emph */ 14 | /* Generic.Error */ 15 | /* Generic.Heading */ 16 | /* Generic.Inserted */ 17 | /* Generic.Inserted.Specific */ 18 | /* Generic.Output */ 19 | /* Generic.Prompt */ 20 | /* Generic.Strong */ 21 | /* Generic.Subheading */ 22 | /* Generic.Traceback */ 23 | /* Keyword.Constant */ 24 | /* Keyword.Declaration */ 25 | /* Keyword.Pseudo */ 26 | /* Keyword.Reserved */ 27 | /* Keyword.Type */ 28 | /* Literal.Number */ 29 | /* Literal.String */ 30 | /* Name.Attribute */ 31 | /* Name.Builtin */ 32 | /* Name.Class */ 33 | /* Name.Constant */ 34 | /* Name.Entity */ 35 | /* Name.Exception */ 36 | /* Name.Function */ 37 | /* Name.Namespace */ 38 | /* Name.Tag */ 39 | /* Name.Variable */ 40 | /* Operator.Word */ 41 | /* Text.Whitespace */ 42 | /* Literal.Number.Float */ 43 | /* Literal.Number.Hex */ 44 | /* Literal.Number.Integer */ 45 | /* Literal.Number.Oct */ 46 | /* Literal.String.Backtick */ 47 | /* Literal.String.Char */ 48 | /* Literal.String.Doc */ 49 | /* Literal.String.Double */ 50 | /* Literal.String.Escape */ 51 | /* Literal.String.Heredoc */ 52 | /* Literal.String.Interpol */ 53 | /* Literal.String.Other */ 54 | /* Literal.String.Regex */ 55 | /* Literal.String.Single */ 56 | /* Literal.String.Symbol */ 57 | /* Name.Builtin.Pseudo */ 58 | /* Name.Variable.Class */ 59 | /* Name.Variable.Global */ 60 | /* Name.Variable.Instance */ 61 | /* Literal.Number.Integer.Long */ } 62 | .highlight .c { 63 | color: #999988; 64 | font-style: italic; } 65 | .highlight .err { 66 | color: #a61717; 67 | background-color: #e3d2d2; } 68 | .highlight .k { 69 | color: #000000; 70 | font-weight: bold; } 71 | .highlight .o { 72 | color: #000000; 73 | font-weight: bold; } 74 | .highlight .cm { 75 | color: #999988; 76 | font-style: italic; } 77 | .highlight .cp { 78 | color: #999999; 79 | font-weight: bold; } 80 | .highlight .c1 { 81 | color: #999988; 82 | font-style: italic; } 83 | .highlight .cs { 84 | color: #999999; 85 | font-weight: bold; 86 | font-style: italic; } 87 | .highlight .gd { 88 | color: #000000; 89 | background-color: #ffdddd; } 90 | .highlight .gd .x { 91 | color: #000000; 92 | background-color: #ffaaaa; } 93 | .highlight .ge { 94 | color: #000000; 95 | font-style: italic; } 96 | .highlight .gr { 97 | color: #aa0000; } 98 | .highlight .gh { 99 | color: #999999; } 100 | .highlight .gi { 101 | color: #000000; 102 | background-color: #ddffdd; } 103 | .highlight .gi .x { 104 | color: #000000; 105 | background-color: #aaffaa; } 106 | .highlight .go { 107 | color: #888888; } 108 | .highlight .gp { 109 | color: #555555; } 110 | .highlight .gs { 111 | font-weight: bold; } 112 | .highlight .gu { 113 | color: #aaaaaa; } 114 | .highlight .gt { 115 | color: #aa0000; } 116 | .highlight .kc { 117 | color: #000000; 118 | font-weight: bold; } 119 | .highlight .kd { 120 | color: #000000; 121 | font-weight: bold; } 122 | .highlight .kp { 123 | color: #000000; 124 | font-weight: bold; } 125 | .highlight .kr { 126 | color: #000000; 127 | font-weight: bold; } 128 | .highlight .kt { 129 | color: #445588; } 130 | .highlight .m { 131 | color: #009999; } 132 | .highlight .s { 133 | color: #d14; } 134 | .highlight .na { 135 | color: #008080; } 136 | .highlight .nb { 137 | color: #0086B3; } 138 | .highlight .nc { 139 | color: #445588; 140 | font-weight: bold; } 141 | .highlight .no { 142 | color: #008080; } 143 | .highlight .ni { 144 | color: #800080; } 145 | .highlight .ne { 146 | color: #990000; 147 | font-weight: bold; } 148 | .highlight .nf { 149 | color: #990000; } 150 | .highlight .nn { 151 | color: #555555; } 152 | .highlight .nt { 153 | color: #000080; } 154 | .highlight .nv { 155 | color: #008080; } 156 | .highlight .ow { 157 | color: #000000; 158 | font-weight: bold; } 159 | .highlight .w { 160 | color: #bbbbbb; } 161 | .highlight .mf { 162 | color: #009999; } 163 | .highlight .mh { 164 | color: #009999; } 165 | .highlight .mi { 166 | color: #009999; } 167 | .highlight .mo { 168 | color: #009999; } 169 | .highlight .sb { 170 | color: #d14; } 171 | .highlight .sc { 172 | color: #d14; } 173 | .highlight .sd { 174 | color: #d14; } 175 | .highlight .s2 { 176 | color: #d14; } 177 | .highlight .se { 178 | color: #d14; } 179 | .highlight .sh { 180 | color: #d14; } 181 | .highlight .si { 182 | color: #d14; } 183 | .highlight .sx { 184 | color: #d14; } 185 | .highlight .sr { 186 | color: #009926; } 187 | .highlight .s1 { 188 | color: #d14; } 189 | .highlight .ss { 190 | color: #990073; } 191 | .highlight .bp { 192 | color: #999999; } 193 | .highlight .vc { 194 | color: #008080; } 195 | .highlight .vg { 196 | color: #008080; } 197 | .highlight .vi { 198 | color: #008080; } 199 | .highlight .il { 200 | color: #009999; } 201 | -------------------------------------------------------------------------------- /docs/docsets/.docset/Contents/Resources/Documents/css/jazzy.css: -------------------------------------------------------------------------------- 1 | *, *:before, *:after { 2 | box-sizing: inherit; } 3 | 4 | body { 5 | margin: 0; 6 | background: #fff; 7 | color: #333; 8 | font: 16px/1.7 "Helvetica Neue", Helvetica, Arial, sans-serif; 9 | letter-spacing: .2px; 10 | -webkit-font-smoothing: antialiased; 11 | box-sizing: border-box; } 12 | 13 | h1 { 14 | font-size: 2rem; 15 | font-weight: 700; 16 | margin: 1.275em 0 0.6em; } 17 | 18 | h2 { 19 | font-size: 1.75rem; 20 | font-weight: 700; 21 | margin: 1.275em 0 0.3em; } 22 | 23 | h3 { 24 | font-size: 1.5rem; 25 | font-weight: 700; 26 | margin: 1em 0 0.3em; } 27 | 28 | h4 { 29 | font-size: 1.25rem; 30 | font-weight: 700; 31 | margin: 1.275em 0 0.85em; } 32 | 33 | h5 { 34 | font-size: 1rem; 35 | font-weight: 700; 36 | margin: 1.275em 0 0.85em; } 37 | 38 | h6 { 39 | font-size: 1rem; 40 | font-weight: 700; 41 | margin: 1.275em 0 0.85em; 42 | color: #777; } 43 | 44 | p { 45 | margin: 0 0 1em; } 46 | 47 | ul, ol { 48 | padding: 0 0 0 2em; 49 | margin: 0 0 0.85em; } 50 | 51 | blockquote { 52 | margin: 0 0 0.85em; 53 | padding: 0 15px; 54 | color: #858585; 55 | border-left: 4px solid #e5e5e5; } 56 | 57 | img { 58 | max-width: 100%; } 59 | 60 | a { 61 | color: #4183c4; 62 | text-decoration: none; } 63 | a:hover, a:focus { 64 | outline: 0; 65 | text-decoration: underline; } 66 | a.discouraged { 67 | text-decoration: line-through; } 68 | a.discouraged:hover, a.discouraged:focus { 69 | text-decoration: underline line-through; } 70 | 71 | table { 72 | background: #fff; 73 | width: 100%; 74 | border-collapse: collapse; 75 | border-spacing: 0; 76 | overflow: auto; 77 | margin: 0 0 0.85em; } 78 | 79 | tr:nth-child(2n) { 80 | background-color: #fbfbfb; } 81 | 82 | th, td { 83 | padding: 6px 13px; 84 | border: 1px solid #ddd; } 85 | 86 | pre { 87 | margin: 0 0 1.275em; 88 | padding: .85em 1em; 89 | overflow: auto; 90 | background: #f7f7f7; 91 | font-size: .85em; 92 | font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; } 93 | 94 | code { 95 | font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; } 96 | 97 | .item-container p > code, .item-container li > code, .top-matter p > code, .top-matter li > code { 98 | background: #f7f7f7; 99 | padding: .2em; } 100 | .item-container p > code:before, .item-container p > code:after, .item-container li > code:before, .item-container li > code:after, .top-matter p > code:before, .top-matter p > code:after, .top-matter li > code:before, .top-matter li > code:after { 101 | letter-spacing: -.2em; 102 | content: "\00a0"; } 103 | 104 | pre code { 105 | padding: 0; 106 | white-space: pre; } 107 | 108 | .content-wrapper { 109 | display: flex; 110 | flex-direction: column; } 111 | @media (min-width: 768px) { 112 | .content-wrapper { 113 | flex-direction: row; } } 114 | 115 | .header { 116 | display: flex; 117 | padding: 8px; 118 | font-size: 0.875em; 119 | background: #444; 120 | color: #999; } 121 | 122 | .header-col { 123 | margin: 0; 124 | padding: 0 8px; } 125 | 126 | .header-col--primary { 127 | flex: 1; } 128 | 129 | .header-link { 130 | color: #fff; } 131 | 132 | .header-icon { 133 | padding-right: 6px; 134 | vertical-align: -4px; 135 | height: 16px; } 136 | 137 | .breadcrumbs { 138 | font-size: 0.875em; 139 | padding: 8px 16px; 140 | margin: 0; 141 | background: #fbfbfb; 142 | border-bottom: 1px solid #ddd; } 143 | 144 | .carat { 145 | height: 10px; 146 | margin: 0 5px; } 147 | 148 | .navigation { 149 | order: 2; } 150 | @media (min-width: 768px) { 151 | .navigation { 152 | order: 1; 153 | width: 25%; 154 | max-width: 300px; 155 | padding-bottom: 64px; 156 | overflow: hidden; 157 | word-wrap: normal; 158 | background: #fbfbfb; 159 | border-right: 1px solid #ddd; } } 160 | 161 | .nav-groups { 162 | list-style-type: none; 163 | padding-left: 0; } 164 | 165 | .nav-group-name { 166 | border-bottom: 1px solid #ddd; 167 | padding: 8px 0 8px 16px; } 168 | 169 | .nav-group-name-link { 170 | color: #333; } 171 | 172 | .nav-group-tasks { 173 | margin: 8px 0; 174 | padding: 0 0 0 8px; } 175 | 176 | .nav-group-task { 177 | font-size: 1em; 178 | list-style-type: none; 179 | white-space: nowrap; } 180 | 181 | .nav-group-task-link { 182 | color: #808080; } 183 | 184 | .main-content { 185 | order: 1; } 186 | @media (min-width: 768px) { 187 | .main-content { 188 | order: 2; 189 | flex: 1; 190 | padding-bottom: 60px; } } 191 | 192 | .section { 193 | padding: 0 32px; 194 | border-bottom: 1px solid #ddd; } 195 | 196 | .section-content { 197 | max-width: 834px; 198 | margin: 0 auto; 199 | padding: 16px 0; } 200 | 201 | .section-name { 202 | color: #666; 203 | display: block; } 204 | .section-name p { 205 | margin-bottom: inherit; } 206 | 207 | .declaration .highlight { 208 | overflow-x: initial; 209 | padding: 8px 0; 210 | margin: 0; 211 | background-color: transparent; 212 | border: none; } 213 | 214 | .task-group-section { 215 | border-top: 1px solid #ddd; } 216 | 217 | .task-group { 218 | padding-top: 0px; } 219 | 220 | .task-name-container a[name]:before { 221 | content: ""; 222 | display: block; } 223 | 224 | .section-name-container { 225 | position: relative; } 226 | .section-name-container .section-name-link { 227 | position: absolute; 228 | top: 0; 229 | left: 0; 230 | bottom: 0; 231 | right: 0; 232 | margin-bottom: 0; } 233 | .section-name-container .section-name { 234 | position: relative; 235 | pointer-events: none; 236 | z-index: 1; } 237 | .section-name-container .section-name a { 238 | pointer-events: auto; } 239 | 240 | .item-container { 241 | padding: 0; } 242 | 243 | .item { 244 | padding-top: 8px; 245 | width: 100%; 246 | list-style-type: none; } 247 | .item a[name]:before { 248 | content: ""; 249 | display: block; } 250 | .item .token, .item .direct-link { 251 | padding-left: 3px; 252 | margin-left: 0px; 253 | font-size: 1rem; } 254 | .item .declaration-note { 255 | font-size: .85em; 256 | color: #808080; 257 | font-style: italic; } 258 | 259 | .pointer-container { 260 | border-bottom: 1px solid #ddd; 261 | left: -23px; 262 | padding-bottom: 13px; 263 | position: relative; 264 | width: 110%; } 265 | 266 | .pointer { 267 | left: 21px; 268 | top: 7px; 269 | display: block; 270 | position: absolute; 271 | width: 12px; 272 | height: 12px; 273 | border-left: 1px solid #ddd; 274 | border-top: 1px solid #ddd; 275 | background: #fff; 276 | transform: rotate(45deg); } 277 | 278 | .height-container { 279 | display: none; 280 | position: relative; 281 | width: 100%; 282 | overflow: hidden; } 283 | .height-container .section { 284 | background: #fff; 285 | border: 1px solid #ddd; 286 | border-top-width: 0; 287 | padding-top: 10px; 288 | padding-bottom: 5px; 289 | padding: 8px 16px; } 290 | 291 | .aside, .language { 292 | padding: 6px 12px; 293 | margin: 12px 0; 294 | border-left: 5px solid #dddddd; 295 | overflow-y: hidden; } 296 | .aside .aside-title, .language .aside-title { 297 | font-size: 9px; 298 | letter-spacing: 2px; 299 | text-transform: uppercase; 300 | padding-bottom: 0; 301 | margin: 0; 302 | color: #aaa; 303 | -webkit-user-select: none; } 304 | .aside p:last-child, .language p:last-child { 305 | margin-bottom: 0; } 306 | 307 | .language { 308 | border-left: 5px solid #cde9f4; } 309 | .language .aside-title { 310 | color: #4183c4; } 311 | 312 | .aside-warning, .aside-deprecated, .aside-unavailable { 313 | border-left: 5px solid #ff6666; } 314 | .aside-warning .aside-title, .aside-deprecated .aside-title, .aside-unavailable .aside-title { 315 | color: #ff0000; } 316 | 317 | .graybox { 318 | border-collapse: collapse; 319 | width: 100%; } 320 | .graybox p { 321 | margin: 0; 322 | word-break: break-word; 323 | min-width: 50px; } 324 | .graybox td { 325 | border: 1px solid #ddd; 326 | padding: 5px 25px 5px 10px; 327 | vertical-align: middle; } 328 | .graybox tr td:first-of-type { 329 | text-align: right; 330 | padding: 7px; 331 | vertical-align: top; 332 | word-break: normal; 333 | width: 40px; } 334 | 335 | .slightly-smaller { 336 | font-size: 0.9em; } 337 | 338 | .footer { 339 | padding: 8px 16px; 340 | background: #444; 341 | color: #ddd; 342 | font-size: 0.8em; } 343 | .footer p { 344 | margin: 8px 0; } 345 | .footer a { 346 | color: #fff; } 347 | 348 | html.dash .header, html.dash .breadcrumbs, html.dash .navigation { 349 | display: none; } 350 | 351 | html.dash .height-container { 352 | display: block; } 353 | 354 | form[role=search] input { 355 | font: 16px/1.7 "Helvetica Neue", Helvetica, Arial, sans-serif; 356 | font-size: 14px; 357 | line-height: 24px; 358 | padding: 0 10px; 359 | margin: 0; 360 | border: none; 361 | border-radius: 1em; } 362 | .loading form[role=search] input { 363 | background: white url(../img/spinner.gif) center right 4px no-repeat; } 364 | 365 | form[role=search] .tt-menu { 366 | margin: 0; 367 | min-width: 300px; 368 | background: #fbfbfb; 369 | color: #333; 370 | border: 1px solid #ddd; } 371 | 372 | form[role=search] .tt-highlight { 373 | font-weight: bold; } 374 | 375 | form[role=search] .tt-suggestion { 376 | font: 16px/1.7 "Helvetica Neue", Helvetica, Arial, sans-serif; 377 | padding: 0 8px; } 378 | form[role=search] .tt-suggestion span { 379 | display: table-cell; 380 | white-space: nowrap; } 381 | form[role=search] .tt-suggestion .doc-parent-name { 382 | width: 100%; 383 | text-align: right; 384 | font-weight: normal; 385 | font-size: 0.9em; 386 | padding-left: 16px; } 387 | 388 | form[role=search] .tt-suggestion:hover, 389 | form[role=search] .tt-suggestion.tt-cursor { 390 | cursor: pointer; 391 | background-color: #4183c4; 392 | color: #fff; } 393 | 394 | form[role=search] .tt-suggestion:hover .doc-parent-name, 395 | form[role=search] .tt-suggestion.tt-cursor .doc-parent-name { 396 | color: #fff; } 397 | -------------------------------------------------------------------------------- /docs/docsets/.docset/Contents/Resources/Documents/img/carat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidask/StateViewController/98f183d417ea6c8a74fa2bb297e0d574c134358b/docs/docsets/.docset/Contents/Resources/Documents/img/carat.png -------------------------------------------------------------------------------- /docs/docsets/.docset/Contents/Resources/Documents/img/dash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidask/StateViewController/98f183d417ea6c8a74fa2bb297e0d574c134358b/docs/docsets/.docset/Contents/Resources/Documents/img/dash.png -------------------------------------------------------------------------------- /docs/docsets/.docset/Contents/Resources/Documents/img/gh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidask/StateViewController/98f183d417ea6c8a74fa2bb297e0d574c134358b/docs/docsets/.docset/Contents/Resources/Documents/img/gh.png -------------------------------------------------------------------------------- /docs/docsets/.docset/Contents/Resources/Documents/img/spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidask/StateViewController/98f183d417ea6c8a74fa2bb297e0d574c134358b/docs/docsets/.docset/Contents/Resources/Documents/img/spinner.gif -------------------------------------------------------------------------------- /docs/docsets/.docset/Contents/Resources/Documents/js/jazzy.js: -------------------------------------------------------------------------------- 1 | window.jazzy = {'docset': false} 2 | if (typeof window.dash != 'undefined') { 3 | document.documentElement.className += ' dash' 4 | window.jazzy.docset = true 5 | } 6 | if (navigator.userAgent.match(/xcode/i)) { 7 | document.documentElement.className += ' xcode' 8 | window.jazzy.docset = true 9 | } 10 | 11 | function toggleItem($link, $content) { 12 | var animationDuration = 300; 13 | $link.toggleClass('token-open'); 14 | $content.slideToggle(animationDuration); 15 | } 16 | 17 | function itemLinkToContent($link) { 18 | return $link.parent().parent().next(); 19 | } 20 | 21 | // On doc load + hash-change, open any targetted item 22 | function openCurrentItemIfClosed() { 23 | if (window.jazzy.docset) { 24 | return; 25 | } 26 | var $link = $(`.token[href="${location.hash}"]`); 27 | $content = itemLinkToContent($link); 28 | if ($content.is(':hidden')) { 29 | toggleItem($link, $content); 30 | } 31 | } 32 | 33 | $(openCurrentItemIfClosed); 34 | $(window).on('hashchange', openCurrentItemIfClosed); 35 | 36 | // On item link ('token') click, toggle its discussion 37 | $('.token').on('click', function(event) { 38 | if (window.jazzy.docset) { 39 | return; 40 | } 41 | var $link = $(this); 42 | toggleItem($link, itemLinkToContent($link)); 43 | 44 | // Keeps the document from jumping to the hash. 45 | var href = $link.attr('href'); 46 | if (history.pushState) { 47 | history.pushState({}, '', href); 48 | } else { 49 | location.hash = href; 50 | } 51 | event.preventDefault(); 52 | }); 53 | 54 | // Clicks on links to the current, closed, item need to open the item 55 | $("a:not('.token')").on('click', function() { 56 | if (location == this.href) { 57 | openCurrentItemIfClosed(); 58 | } 59 | }); 60 | -------------------------------------------------------------------------------- /docs/docsets/.docset/Contents/Resources/Documents/js/jazzy.search.js: -------------------------------------------------------------------------------- 1 | $(function(){ 2 | var $typeahead = $('[data-typeahead]'); 3 | var $form = $typeahead.parents('form'); 4 | var searchURL = $form.attr('action'); 5 | 6 | function displayTemplate(result) { 7 | return result.name; 8 | } 9 | 10 | function suggestionTemplate(result) { 11 | var t = '
'; 12 | t += '' + result.name + ''; 13 | if (result.parent_name) { 14 | t += '' + result.parent_name + ''; 15 | } 16 | t += '
'; 17 | return t; 18 | } 19 | 20 | $typeahead.one('focus', function() { 21 | $form.addClass('loading'); 22 | 23 | $.getJSON(searchURL).then(function(searchData) { 24 | const searchIndex = lunr(function() { 25 | this.ref('url'); 26 | this.field('name'); 27 | this.field('abstract'); 28 | for (const [url, doc] of Object.entries(searchData)) { 29 | this.add({url: url, name: doc.name, abstract: doc.abstract}); 30 | } 31 | }); 32 | 33 | $typeahead.typeahead( 34 | { 35 | highlight: true, 36 | minLength: 3, 37 | autoselect: true 38 | }, 39 | { 40 | limit: 10, 41 | display: displayTemplate, 42 | templates: { suggestion: suggestionTemplate }, 43 | source: function(query, sync) { 44 | const lcSearch = query.toLowerCase(); 45 | const results = searchIndex.query(function(q) { 46 | q.term(lcSearch, { boost: 100 }); 47 | q.term(lcSearch, { 48 | boost: 10, 49 | wildcard: lunr.Query.wildcard.TRAILING 50 | }); 51 | }).map(function(result) { 52 | var doc = searchData[result.ref]; 53 | doc.url = result.ref; 54 | return doc; 55 | }); 56 | sync(results); 57 | } 58 | } 59 | ); 60 | $form.removeClass('loading'); 61 | $typeahead.trigger('focus'); 62 | }); 63 | }); 64 | 65 | var baseURL = searchURL.slice(0, -"search.json".length); 66 | 67 | $typeahead.on('typeahead:select', function(e, result) { 68 | window.location = baseURL + result.url; 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /docs/docsets/.docset/Contents/Resources/Documents/search.json: -------------------------------------------------------------------------------- 1 | {"Protocols/StateViewControllerTransitioning.html#/stateTransitionDuration(isAppearing:)":{"name":"stateTransitionDuration(isAppearing:)","abstract":"

Returns the animation duration for a state transition of this view controller.

","parent_name":"StateViewControllerTransitioning"},"Protocols/StateViewControllerTransitioning.html#/stateTransitionWillBegin(isAppearing:)":{"name":"stateTransitionWillBegin(isAppearing:)","abstract":"

Notifies that a state transition will begin for this view controller.

","parent_name":"StateViewControllerTransitioning"},"Protocols/StateViewControllerTransitioning.html#/stateTransitionDidEnd(isAppearing:)":{"name":"stateTransitionDidEnd(isAppearing:)","abstract":"

Notifies that a state transition did end for this view controller.

","parent_name":"StateViewControllerTransitioning"},"Protocols/StateViewControllerTransitioning.html#/animateAlongsideStateTransition(isAppearing:)":{"name":"animateAlongsideStateTransition(isAppearing:)","abstract":"

Animations performed alongside the state transition of this view controller.

","parent_name":"StateViewControllerTransitioning"},"Protocols/StateViewControllerTransitioning.html#/stateTransitionDelay(isAppearing:)":{"name":"stateTransitionDelay(isAppearing:)","abstract":"

Returns the animation delay for a state transition of the provided view controller.

","parent_name":"StateViewControllerTransitioning"},"Protocols/StateViewControllerTransitioning.html":{"name":"StateViewControllerTransitioning","abstract":"

View controllers can conform to this protocol to provide their desired"},"Classes/StateViewController.html#/childForStatusBarStyle":{"name":"childForStatusBarStyle","parent_name":"StateViewController"},"Classes/StateViewController.html#/childForStatusBarHidden":{"name":"childForStatusBarHidden","parent_name":"StateViewController"},"Classes/StateViewController.html#/isTransitioningBetweenStates":{"name":"isTransitioningBetweenStates","abstract":"

Indicates whether the view controller currently is transitioning between states.

","parent_name":"StateViewController"},"Classes/StateViewController.html#/currentState":{"name":"currentState","abstract":"

Indicates the current state, or invokes loadAppearanceState() is a current state transition has not","parent_name":"StateViewController"},"Classes/StateViewController.html#/hasDeterminedState":{"name":"hasDeterminedState","abstract":"

Indicates whether the state of this view controller has been determined.","parent_name":"StateViewController"},"Classes/StateViewController.html#/loadAppearanceState()":{"name":"loadAppearanceState()","abstract":"

Loads a state that should represent this view controller immediately as this view controller","parent_name":"StateViewController"},"Classes/StateViewController.html#/setNeedsStateTransition(to:animated:)":{"name":"setNeedsStateTransition(to:animated:)","abstract":"

Notifies the state view controller that a new state is needed.","parent_name":"StateViewController"},"Classes/StateViewController.html#/children(for:)":{"name":"children(for:)","abstract":"

Returns an array of content view controllers representing a state.","parent_name":"StateViewController"},"Classes/StateViewController.html#/childContainerView":{"name":"childContainerView","abstract":"

Container view placed directly in the StateViewControllers view.","parent_name":"StateViewController"},"Classes/StateViewController.html#/loadChildContainerView()":{"name":"loadChildContainerView()","abstract":"

","parent_name":"StateViewController"},"Classes/StateViewController.html#/willTransition(to:animated:)":{"name":"willTransition(to:animated:)","abstract":"

Notifies the view controller that a state transition is to be performed.

","parent_name":"StateViewController"},"Classes/StateViewController.html#/didTransition(from:animated:)":{"name":"didTransition(from:animated:)","abstract":"

Notifies the view controller that it has finished transitioning to a new state.

","parent_name":"StateViewController"},"Classes/StateViewController.html#/childWillAppear(_:animated:)":{"name":"childWillAppear(_:animated:)","abstract":"

Notifies the view controller that a content view controller will appear.

","parent_name":"StateViewController"},"Classes/StateViewController.html#/childDidAppear(_:animated:)":{"name":"childDidAppear(_:animated:)","abstract":"

Notifies the view controller that a content view controller did appear.

","parent_name":"StateViewController"},"Classes/StateViewController.html#/childWillDisappear(_:animated:)":{"name":"childWillDisappear(_:animated:)","abstract":"

Notifies the view controller that a content view controller will disappear.","parent_name":"StateViewController"},"Classes/StateViewController.html#/childDidDisappear(_:animated:)":{"name":"childDidDisappear(_:animated:)","abstract":"

Notifies the view controller that a content view controller did disappear.

","parent_name":"StateViewController"},"Classes/StateViewController.html":{"name":"StateViewController","abstract":"

A container view controller that manages the appearance of one or more child view controller for any given state.

"},"Classes.html":{"name":"Classes","abstract":"

The following classes are available globally.

"},"Protocols.html":{"name":"Protocols","abstract":"

The following protocols are available globally.

"}} -------------------------------------------------------------------------------- /docs/docsets/.docset/Contents/Resources/docSet.dsidx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidask/StateViewController/98f183d417ea6c8a74fa2bb297e0d574c134358b/docs/docsets/.docset/Contents/Resources/docSet.dsidx -------------------------------------------------------------------------------- /docs/docsets/.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidask/StateViewController/98f183d417ea6c8a74fa2bb297e0d574c134358b/docs/docsets/.tgz -------------------------------------------------------------------------------- /docs/img/carat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidask/StateViewController/98f183d417ea6c8a74fa2bb297e0d574c134358b/docs/img/carat.png -------------------------------------------------------------------------------- /docs/img/dash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidask/StateViewController/98f183d417ea6c8a74fa2bb297e0d574c134358b/docs/img/dash.png -------------------------------------------------------------------------------- /docs/img/gh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidask/StateViewController/98f183d417ea6c8a74fa2bb297e0d574c134358b/docs/img/gh.png -------------------------------------------------------------------------------- /docs/img/spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidask/StateViewController/98f183d417ea6c8a74fa2bb297e0d574c134358b/docs/img/spinner.gif -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Reference 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 |

22 | 23 | Docs 24 | 25 | (0% documented) 26 |

27 | 28 |

29 |

30 | 31 |
32 |

33 | 34 |

35 | 36 | 37 | View on GitHub 38 | 39 |

40 | 41 |
42 | 43 | 48 | 49 |
50 | 70 |
71 | 72 |
73 |
74 | 75 |

Build

76 |

StateViewController

77 | 78 |

When creating rich view controllers, a single view controller class is often tasked with managing the appearance of many other views, controls, and other user interface elements based on a state. That state, in turn, is often derived from multiple sources that need to be synchronized to correctly represent a single reliable state. Usually the end result is known as the Massive View Controller problem, often solved by attempts to abandon the MVC pattern, the primary design pattern in UIKit. While other patterns, such as MVVM or MVP, can solve some issues, going with the grain rather than against makes interacting with UIKit more accommodating.

79 | 80 |

This repository houses a UIViewController subclass, enabling modularization and decoupling of view controllers, reducing the size of individual view controllers substantially, without the need for abandoning MVC as a design pattern.

81 |

Requirements

82 | 83 |
    84 |
  • iOS 8.0+
  • 85 |
  • tvOS 9.0+
  • 86 |
87 |

Overview

88 | 89 |

StateViewController is a container view controller that presents one or more view controllers for any given state that you define, such as loading, list, or editing. It manages the appearance cycles of each child view controller, making sure that the view life cycle of the child view controllers are intact and in order, notifying you about state transitions and which child view controllers are about to appear or disappear from the view hierarchy. This allows you to compose multiple view controllers and re-use them throughout the app. The state view controller also provides extensive support for animating the transition between states.

90 | 91 |

Which view controller(s) are visible on screen is dictated by children(for:).

92 |

State transitions during appearance transition

93 | 94 |

When presented on screen, the a state view controller requires an initial state as a starting point. During its appearance transition, the loadAppearanceState() method is invoked to query the state appropriate to transition to as the state view controller appears on screen. 95 | If the appearance transition is animated, the state transition animation is respected, and target child view controllers have the option to appear asynchronously. If the appearance transition is not animated, all child view controllers are immediately placed on screen.

96 | 97 |

loadAppearanceState() must execute synchronously, and is a good place to query any persistence layer for available data, determining whether a final state is ready.

98 | 99 |

During appearance cycle

100 |

State transitions while on screen

101 | 102 |

When on-screen, invoking setNeedsTransition:to: will trigger a transition from the current state to the target state. A common practice is to have the transition from one state to another to trigger an an asynchronous operation (such as a network call), which upon completion, requests a third state based on the success of the asynchronous operation.

103 | 104 |

Between appearance cycle

105 |

Documentation

106 | 107 |

The source code documentation can be found here.

108 |

Installation

109 | 110 |

This module is available via Carthage. Modify your Cartfile to include StateViewController:

111 |
github "davidask/StateViewController"
112 | 
113 |

Usage

114 |
import StateViewController
115 | 
116 |

Subclassing StateViewController

117 | 118 |

To use StateViewController you must override it. The class specifies a generic with a subtype of State. The state type can be designed to house the actual model data required by your view controller, but that’s an optional design decision. For instance, you can create a state that simply determines an abstract state:

119 |
enum MyState {
120 |     case loading
121 |     case ready
122 |     case error
123 | }
124 | 
125 | 126 |

Or, you can define a state which in itself contains model data:

127 |
enum MyState {
128 |     case loading
129 |     case ready(MyModel)
130 |     case error(Error)
131 | }
132 | 
133 | 134 |

Once you have a state, create a subclass of StateViewController.

135 |
class MyStateViewController: StateViewController<MyState>
136 | 
137 | 138 |

Each time StateViewController is about to appear on screen it will call its loadAppearanceState() method. This method returns a state which should be ready for display as soon as the view controller is on screen. Override this method to determine what state is appropriate to display immediately, depending on cached data or the contents of your database.

139 |
override func loadAppearanceState() -> MyState {
140 |     if let myModel = MyCache.cachedModel {
141 |         return .ready(myModel)
142 |     } else {
143 |         return .loading
144 |     }
145 | }
146 | 
147 | 148 |

Each state can be represented by zero or more view controllers. To provide which view controllers are visible for what state, override children(for:).

149 |
override func children(for state: MyState) -> [UIViewController] {
150 |     switch state {
151 |         case .loading:
152 |             return [ActivityIndicatorViewController()]
153 |         case .ready:
154 |             return [myTableViewController]
155 |         case .error:
156 |             return [ErrorViewController()]
157 |     }
158 | }
159 | 
160 | 161 |

You receive callbacks for when a state transition will begin, and when it has finished. 162 | willTransition(to:animated:) is a good place to prepare your child view controllers for appearance.

163 |
override func willTransition(to nextState: MyState, animated: Bool) {
164 |     switch nextState {
165 |         case .ready(let model):
166 |             navigationItem.setRightBarButton(myBarButtonItem, animated: animated)
167 |             myTableViewController.setRows(model.objects)
168 |         default:
169 |             navigationItem.setRightBarButton(nil, animated: animated)
170 |     }
171 | }
172 | 
173 | 174 |

When didTransition(from:animated:) is called, a state transition has finished successfully. This is a good time to invoke other methods which in turn will trigger another state transition.

175 |
override func didTransition(from previousState: State?, animated: Bool) {
176 |     switch currentState {
177 |         case .loading:
178 |             fetchData { model in
179 |                 self.setNeedsTransition(to: .ready(model), animated: true)
180 |             }
181 |         defualt:
182 |             break
183 |     }
184 | }
185 | 
186 | 187 |

Your StateViewController is now ready, and will switch between view controllers depending on state. Using setNeedsTransition(:to:animated:) you can transition between various states during the life cycle of your state view controller subclass.

188 | 189 |

Multiple other callbacks are available for determining when a child view controller is appearing or disappearing. Please reference the documentation or the Example.

190 |

Providing transitions between child view controllers

191 | 192 |

Child view controllers of StateViewController conforming to the StateViewControllerTransitioning protocol can individually control their own transition. The available methods provide functionality for:

193 | 194 |
    195 |
  • Specifying duration and delayed start of an animated transition
  • 196 |
  • Preparing the view controller for presentation
  • 197 |
  • Performing animations along side an animated transition
  • 198 |
  • Performing operations after a transition
  • 199 |
200 |

Example

201 | 202 |

In the example application included in this project the state view controller switches between two view controllers. Firstly, it displays and animates the transition of an activity indicator view controller while a network call is being performed. Once the network call is successfully completed it transitions into a state displaying a table view with the loaded content.

203 |

Contribute

204 | 205 |

Please feel welcome contributing to StateViewController, check the LICENSE file for more info.

206 |

Credits

207 | 208 |

David Ask

209 | 210 |
211 |
212 | 213 | 214 |
215 |
216 | 220 | 221 |
222 | 223 | -------------------------------------------------------------------------------- /docs/js/jazzy.js: -------------------------------------------------------------------------------- 1 | window.jazzy = {'docset': false} 2 | if (typeof window.dash != 'undefined') { 3 | document.documentElement.className += ' dash' 4 | window.jazzy.docset = true 5 | } 6 | if (navigator.userAgent.match(/xcode/i)) { 7 | document.documentElement.className += ' xcode' 8 | window.jazzy.docset = true 9 | } 10 | 11 | function toggleItem($link, $content) { 12 | var animationDuration = 300; 13 | $link.toggleClass('token-open'); 14 | $content.slideToggle(animationDuration); 15 | } 16 | 17 | function itemLinkToContent($link) { 18 | return $link.parent().parent().next(); 19 | } 20 | 21 | // On doc load + hash-change, open any targetted item 22 | function openCurrentItemIfClosed() { 23 | if (window.jazzy.docset) { 24 | return; 25 | } 26 | var $link = $(`.token[href="${location.hash}"]`); 27 | $content = itemLinkToContent($link); 28 | if ($content.is(':hidden')) { 29 | toggleItem($link, $content); 30 | } 31 | } 32 | 33 | $(openCurrentItemIfClosed); 34 | $(window).on('hashchange', openCurrentItemIfClosed); 35 | 36 | // On item link ('token') click, toggle its discussion 37 | $('.token').on('click', function(event) { 38 | if (window.jazzy.docset) { 39 | return; 40 | } 41 | var $link = $(this); 42 | toggleItem($link, itemLinkToContent($link)); 43 | 44 | // Keeps the document from jumping to the hash. 45 | var href = $link.attr('href'); 46 | if (history.pushState) { 47 | history.pushState({}, '', href); 48 | } else { 49 | location.hash = href; 50 | } 51 | event.preventDefault(); 52 | }); 53 | 54 | // Clicks on links to the current, closed, item need to open the item 55 | $("a:not('.token')").on('click', function() { 56 | if (location == this.href) { 57 | openCurrentItemIfClosed(); 58 | } 59 | }); 60 | -------------------------------------------------------------------------------- /docs/js/jazzy.search.js: -------------------------------------------------------------------------------- 1 | $(function(){ 2 | var $typeahead = $('[data-typeahead]'); 3 | var $form = $typeahead.parents('form'); 4 | var searchURL = $form.attr('action'); 5 | 6 | function displayTemplate(result) { 7 | return result.name; 8 | } 9 | 10 | function suggestionTemplate(result) { 11 | var t = '
'; 12 | t += '' + result.name + ''; 13 | if (result.parent_name) { 14 | t += '' + result.parent_name + ''; 15 | } 16 | t += '
'; 17 | return t; 18 | } 19 | 20 | $typeahead.one('focus', function() { 21 | $form.addClass('loading'); 22 | 23 | $.getJSON(searchURL).then(function(searchData) { 24 | const searchIndex = lunr(function() { 25 | this.ref('url'); 26 | this.field('name'); 27 | this.field('abstract'); 28 | for (const [url, doc] of Object.entries(searchData)) { 29 | this.add({url: url, name: doc.name, abstract: doc.abstract}); 30 | } 31 | }); 32 | 33 | $typeahead.typeahead( 34 | { 35 | highlight: true, 36 | minLength: 3, 37 | autoselect: true 38 | }, 39 | { 40 | limit: 10, 41 | display: displayTemplate, 42 | templates: { suggestion: suggestionTemplate }, 43 | source: function(query, sync) { 44 | const lcSearch = query.toLowerCase(); 45 | const results = searchIndex.query(function(q) { 46 | q.term(lcSearch, { boost: 100 }); 47 | q.term(lcSearch, { 48 | boost: 10, 49 | wildcard: lunr.Query.wildcard.TRAILING 50 | }); 51 | }).map(function(result) { 52 | var doc = searchData[result.ref]; 53 | doc.url = result.ref; 54 | return doc; 55 | }); 56 | sync(results); 57 | } 58 | } 59 | ); 60 | $form.removeClass('loading'); 61 | $typeahead.trigger('focus'); 62 | }); 63 | }); 64 | 65 | var baseURL = searchURL.slice(0, -"search.json".length); 66 | 67 | $typeahead.on('typeahead:select', function(e, result) { 68 | window.location = baseURL + result.url; 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /docs/search.json: -------------------------------------------------------------------------------- 1 | {"Protocols/StateViewControllerTransitioning.html#/stateTransitionDuration(isAppearing:)":{"name":"stateTransitionDuration(isAppearing:)","abstract":"

Returns the animation duration for a state transition of this view controller.

","parent_name":"StateViewControllerTransitioning"},"Protocols/StateViewControllerTransitioning.html#/stateTransitionWillBegin(isAppearing:)":{"name":"stateTransitionWillBegin(isAppearing:)","abstract":"

Notifies that a state transition will begin for this view controller.

","parent_name":"StateViewControllerTransitioning"},"Protocols/StateViewControllerTransitioning.html#/stateTransitionDidEnd(isAppearing:)":{"name":"stateTransitionDidEnd(isAppearing:)","abstract":"

Notifies that a state transition did end for this view controller.

","parent_name":"StateViewControllerTransitioning"},"Protocols/StateViewControllerTransitioning.html#/animateAlongsideStateTransition(isAppearing:)":{"name":"animateAlongsideStateTransition(isAppearing:)","abstract":"

Animations performed alongside the state transition of this view controller.

","parent_name":"StateViewControllerTransitioning"},"Protocols/StateViewControllerTransitioning.html#/stateTransitionDelay(isAppearing:)":{"name":"stateTransitionDelay(isAppearing:)","abstract":"

Returns the animation delay for a state transition of the provided view controller.

","parent_name":"StateViewControllerTransitioning"},"Protocols/StateViewControllerTransitioning.html":{"name":"StateViewControllerTransitioning","abstract":"

View controllers can conform to this protocol to provide their desired"},"Classes/StateViewController.html#/childForStatusBarStyle":{"name":"childForStatusBarStyle","parent_name":"StateViewController"},"Classes/StateViewController.html#/childForStatusBarHidden":{"name":"childForStatusBarHidden","parent_name":"StateViewController"},"Classes/StateViewController.html#/isTransitioningBetweenStates":{"name":"isTransitioningBetweenStates","abstract":"

Indicates whether the view controller currently is transitioning between states.

","parent_name":"StateViewController"},"Classes/StateViewController.html#/currentState":{"name":"currentState","abstract":"

Indicates the current state, or invokes loadAppearanceState() is a current state transition has not","parent_name":"StateViewController"},"Classes/StateViewController.html#/hasDeterminedState":{"name":"hasDeterminedState","abstract":"

Indicates whether the state of this view controller has been determined.","parent_name":"StateViewController"},"Classes/StateViewController.html#/loadAppearanceState()":{"name":"loadAppearanceState()","abstract":"

Loads a state that should represent this view controller immediately as this view controller","parent_name":"StateViewController"},"Classes/StateViewController.html#/setNeedsStateTransition(to:animated:)":{"name":"setNeedsStateTransition(to:animated:)","abstract":"

Notifies the state view controller that a new state is needed.","parent_name":"StateViewController"},"Classes/StateViewController.html#/children(for:)":{"name":"children(for:)","abstract":"

Returns an array of content view controllers representing a state.","parent_name":"StateViewController"},"Classes/StateViewController.html#/childContainerView":{"name":"childContainerView","abstract":"

Container view placed directly in the StateViewControllers view.","parent_name":"StateViewController"},"Classes/StateViewController.html#/loadChildContainerView()":{"name":"loadChildContainerView()","abstract":"

","parent_name":"StateViewController"},"Classes/StateViewController.html#/willTransition(to:animated:)":{"name":"willTransition(to:animated:)","abstract":"

Notifies the view controller that a state transition is to be performed.

","parent_name":"StateViewController"},"Classes/StateViewController.html#/didTransition(from:animated:)":{"name":"didTransition(from:animated:)","abstract":"

Notifies the view controller that it has finished transitioning to a new state.

","parent_name":"StateViewController"},"Classes/StateViewController.html#/childWillAppear(_:animated:)":{"name":"childWillAppear(_:animated:)","abstract":"

Notifies the view controller that a content view controller will appear.

","parent_name":"StateViewController"},"Classes/StateViewController.html#/childDidAppear(_:animated:)":{"name":"childDidAppear(_:animated:)","abstract":"

Notifies the view controller that a content view controller did appear.

","parent_name":"StateViewController"},"Classes/StateViewController.html#/childWillDisappear(_:animated:)":{"name":"childWillDisappear(_:animated:)","abstract":"

Notifies the view controller that a content view controller will disappear.","parent_name":"StateViewController"},"Classes/StateViewController.html#/childDidDisappear(_:animated:)":{"name":"childDidDisappear(_:animated:)","abstract":"

Notifies the view controller that a content view controller did disappear.

","parent_name":"StateViewController"},"Classes/StateViewController.html":{"name":"StateViewController","abstract":"

A container view controller that manages the appearance of one or more child view controller for any given state.

"},"Classes.html":{"name":"Classes","abstract":"

The following classes are available globally.

"},"Protocols.html":{"name":"Protocols","abstract":"

The following protocols are available globally.

"}} -------------------------------------------------------------------------------- /docs/undocumented.json: -------------------------------------------------------------------------------- 1 | { 2 | "warnings": [ 3 | 4 | ], 5 | "source_directory": "/Users/davidask/Github/davidask/StateViewController" 6 | } -------------------------------------------------------------------------------- /images/between-lifecycle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidask/StateViewController/98f183d417ea6c8a74fa2bb297e0d574c134358b/images/between-lifecycle.png -------------------------------------------------------------------------------- /images/during-lifecycle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidask/StateViewController/98f183d417ea6c8a74fa2bb297e0d574c134358b/images/during-lifecycle.png --------------------------------------------------------------------------------