├── .gitignore ├── .swift-version ├── .swiftlint.yml ├── ArchitectureComponents.podspec ├── ArchitectureComponents.xcodeproj ├── project.pbxproj └── xcshareddata │ └── xcschemes │ └── ArchitectureComponents.xcscheme ├── ArchitectureComponents.xcworkspace └── contents.xcworkspacedata ├── CODEOFCONDUCT.md ├── Cartfile ├── Cartfile.private ├── Cartfile.resolved ├── LICENSE ├── Package.swift ├── README.md ├── Sources ├── ArchitectureComponents │ ├── Lifecycle │ │ ├── DefaultLifecycleObserver.swift │ │ ├── Lifecycle.swift │ │ ├── LifecycleObserver.swift │ │ ├── LifecycleOwner.swift │ │ ├── LifecycleRegistry.swift │ │ └── Swift │ │ │ ├── Extensions.swift │ │ │ └── LifecycleError.swift │ ├── LiveData │ │ ├── LiveData.swift │ │ ├── MediatorLiveData.swift │ │ ├── MutableLiveData.swift │ │ ├── Swift │ │ │ ├── LiveDataError.swift │ │ │ ├── ObserverController.swift │ │ │ ├── ObserverHandle.swift │ │ │ ├── ObserverListController.swift │ │ │ ├── SwitchedLiveData.swift │ │ │ └── TransformedLiveData.swift │ │ └── Transformations.swift │ └── UIKit │ │ ├── LifecycleTableViewController.swift │ │ └── LifecycleViewController.swift └── Info.plist ├── Tests ├── ArchitectureComponentsTests │ ├── Lifecycle │ │ └── LifecycleStateTests.swift │ └── LiveData │ │ ├── Helpers │ │ ├── MockLifecycleOwner.swift │ │ └── MockLiveDataObserver.swift │ │ ├── LiveData_NotificationTests.swift │ │ ├── LiveData_ObserverManagementTests.swift │ │ └── MutableLiveData_PostValueTests.swift ├── Info.plist └── LinuxMain.swift └── bin └── setup /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/carthage,cocoapods,linux,macos,swift,swiftpackagemanager,xcode 3 | 4 | ### Carthage ### 5 | # Carthage 6 | # 7 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 8 | # Carthage/Checkouts 9 | 10 | Carthage/Build 11 | 12 | ### CocoaPods ### 13 | ## CocoaPods GitIgnore Template 14 | 15 | # CocoaPods - Only use to conserve bandwidth / Save time on Pushing 16 | # - Also handy if you have a lage number of dependant pods 17 | # - AS PER https://guides.cocoapods.org/using/using-cocoapods.html NEVER IGONRE THE LOCK FILE 18 | Pods/ 19 | 20 | ### Linux ### 21 | *~ 22 | 23 | # temporary files which can be created if a process still has a handle open of a deleted file 24 | .fuse_hidden* 25 | 26 | # KDE directory preferences 27 | .directory 28 | 29 | # Linux trash folder which might appear on any partition or disk 30 | .Trash-* 31 | 32 | # .nfs files are created when an open file is removed but is still being accessed 33 | .nfs* 34 | 35 | ### macOS ### 36 | *.DS_Store 37 | .AppleDouble 38 | .LSOverride 39 | 40 | # Icon must end with two \r 41 | Icon 42 | 43 | # Thumbnails 44 | ._* 45 | 46 | # Files that might appear in the root of a volume 47 | .DocumentRevisions-V100 48 | .fseventsd 49 | .Spotlight-V100 50 | .TemporaryItems 51 | .Trashes 52 | .VolumeIcon.icns 53 | .com.apple.timemachine.donotpresent 54 | 55 | # Directories potentially created on remote AFP share 56 | .AppleDB 57 | .AppleDesktop 58 | Network Trash Folder 59 | Temporary Items 60 | .apdisk 61 | 62 | ### Swift ### 63 | # Xcode 64 | # 65 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 66 | 67 | ## Build generated 68 | build/ 69 | DerivedData/ 70 | 71 | ## Various settings 72 | *.pbxuser 73 | !default.pbxuser 74 | *.mode1v3 75 | !default.mode1v3 76 | *.mode2v3 77 | !default.mode2v3 78 | *.perspectivev3 79 | !default.perspectivev3 80 | xcuserdata/ 81 | 82 | ## Other 83 | *.moved-aside 84 | *.xccheckout 85 | *.xcscmblueprint 86 | 87 | ## Obj-C/Swift specific 88 | *.hmap 89 | *.ipa 90 | *.dSYM.zip 91 | *.dSYM 92 | 93 | ## Playgrounds 94 | timeline.xctimeline 95 | playground.xcworkspace 96 | 97 | # Swift Package Manager 98 | # 99 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 100 | # Packages/ 101 | # Package.pins 102 | .build/ 103 | 104 | # CocoaPods - Refactored to standalone file 105 | 106 | # Carthage - Refactored to standalone file 107 | 108 | # fastlane 109 | # 110 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 111 | # screenshots whenever they are needed. 112 | # For more information about the recommended setup visit: 113 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 114 | 115 | fastlane/report.xml 116 | fastlane/Preview.html 117 | fastlane/screenshots 118 | fastlane/test_output 119 | 120 | ### SwiftPackageManager ### 121 | Packages 122 | xcuserdata 123 | 124 | ### Xcode ### 125 | # Xcode 126 | # 127 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 128 | 129 | ## Build generated 130 | 131 | ## Various settings 132 | 133 | ## Other 134 | 135 | ### Xcode Patch ### 136 | *.xcodeproj/* 137 | !*.xcodeproj/project.pbxproj 138 | !*.xcodeproj/xcshareddata/ 139 | !*.xcworkspace/contents.xcworkspacedata 140 | /*.gcno 141 | 142 | # End of https://www.gitignore.io/api/carthage,cocoapods,linux,macos,swift,swiftpackagemanager,xcode -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 4.0 2 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: # rule identifiers to exclude from running 2 | - file_length 3 | - operator_whitespace 4 | - todo 5 | opt_in_rules: # some rules are only opt-in 6 | - array_init 7 | - closure_end_indentation 8 | - closure_spacing 9 | - contains_over_first_not_nil 10 | - control_statement 11 | - empty_count 12 | - explicit_init 13 | - explicit_top_level_acl 14 | - explicit_type_interface 15 | - extension_access_modifier 16 | - fatal_error_message 17 | - first_where 18 | - force_unwrapping 19 | - implicit_return 20 | - implicitly_unwrapped_optional 21 | - let_var_whitespace 22 | - literal_expression_end_indentation 23 | - multiline_arguments 24 | - multiline_parameters 25 | - no_grouping_extension 26 | - number_separator 27 | - object_literal 28 | - operator_usage_whitespace 29 | - overridden_super_call 30 | - pattern_matching_keywords 31 | - private_outlet 32 | - private_unit_test 33 | - prohibited_super_call 34 | - redundant_nil_coalescing 35 | - single_test_class 36 | # - sorted_first_last # rule is not found in 2017-11-10 build of SwiftLint 37 | # - sorted_imports # docs incorrect: placing comment between two imports does not reset the ordering calculation 38 | # - trailing_closure # rule does not ignore functional methods, which SHOULD use () to enable chaining 39 | - vertical_parameter_alignment_on_call 40 | included: # paths to include during linting. `--path` is ignored if present. 41 | - Sources 42 | excluded: # paths to ignore during linting. Takes precedence over `included`. 43 | - Carthage 44 | - Pods 45 | - Vendor 46 | - bin 47 | 48 | reporter: "emoji" # reporter type (xcode, json, csv, checkstyle, junit, html, emoji) 49 | 50 | # rule configuration 51 | identifier_name: 52 | excluded: 53 | - cmd 54 | - id 55 | - url 56 | - vc 57 | line_length: 58 | warning: 132 59 | type_name: 60 | max_length: 61 | warning: 60 62 | vertical_whitespace: 63 | max_empty_lines: 2 64 | -------------------------------------------------------------------------------- /ArchitectureComponents.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "ArchitectureComponents" 3 | s.version = "0.1.0" 4 | s.summary = "A port of Android Architecture Components to iOS." 5 | 6 | s.description = <<-DESC 7 | Provide Lifecycle, LiveData, and other Android Architecture Components on 8 | iOS. Since iOS lacks a first-party application architecture and Android 9 | now has a very nice first-party application architecture, it seems 10 | reasonable to adopt Android's architecture on both platforms in instances 11 | when app delivery would benefit from sharing an app architecture. 12 | DESC 13 | 14 | s.homepage = "https://github.com/spropensource/ArchitectureComponents" 15 | s.license = { :type => "Apache License, Version 2.0", :file => "LICENSE" } 16 | s.authors = { 17 | "David Kinney" => "david.kinney@spr.com" 18 | } 19 | s.platform = :ios, "10.0" 20 | s.source = { :git => "https://github.com/spropensource/ArchitectureComponents.git", :tag => "#{s.version}" } 21 | s.source_files = "Sources/**/*.swift" 22 | end 23 | -------------------------------------------------------------------------------- /ArchitectureComponents.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 48; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | F063F7261FCD2AB200B51878 /* ArchitectureComponents.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F0C92BB81FCD28B7005BA837 /* ArchitectureComponents.framework */; }; 11 | F063F72E1FCD2B1500B51878 /* LifecycleStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0C92C051FCD29C1005BA837 /* LifecycleStateTests.swift */; }; 12 | F063F72F1FCD2B1900B51878 /* LiveData_NotificationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0C92C091FCD29C1005BA837 /* LiveData_NotificationTests.swift */; }; 13 | F063F7301FCD2B1900B51878 /* LiveData_ObserverManagementTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0C92C071FCD29C1005BA837 /* LiveData_ObserverManagementTests.swift */; }; 14 | F063F7311FCD2B1900B51878 /* MutableLiveData_PostValueTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0C92C081FCD29C1005BA837 /* MutableLiveData_PostValueTests.swift */; }; 15 | F063F7321FCD2B1C00B51878 /* MockLifecycleOwner.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0C92C0B1FCD29C1005BA837 /* MockLifecycleOwner.swift */; }; 16 | F063F7331FCD2B1C00B51878 /* MockLiveDataObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0C92C0C1FCD29C1005BA837 /* MockLiveDataObserver.swift */; }; 17 | F0C92BEE1FCD29BA005BA837 /* LifecycleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0C92BD71FCD29BA005BA837 /* LifecycleViewController.swift */; }; 18 | F0C92BEF1FCD29BA005BA837 /* LifecycleTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0C92BD81FCD29BA005BA837 /* LifecycleTableViewController.swift */; }; 19 | F0C92BF01FCD29BA005BA837 /* LifecycleOwner.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0C92BDA1FCD29BA005BA837 /* LifecycleOwner.swift */; }; 20 | F0C92BF11FCD29BA005BA837 /* Lifecycle.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0C92BDB1FCD29BA005BA837 /* Lifecycle.swift */; }; 21 | F0C92BF21FCD29BA005BA837 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0C92BDD1FCD29BA005BA837 /* Extensions.swift */; }; 22 | F0C92BF31FCD29BA005BA837 /* LifecycleError.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0C92BDE1FCD29BA005BA837 /* LifecycleError.swift */; }; 23 | F0C92BF41FCD29BA005BA837 /* DefaultLifecycleObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0C92BDF1FCD29BA005BA837 /* DefaultLifecycleObserver.swift */; }; 24 | F0C92BF51FCD29BA005BA837 /* LifecycleRegistry.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0C92BE01FCD29BA005BA837 /* LifecycleRegistry.swift */; }; 25 | F0C92BF61FCD29BA005BA837 /* LifecycleObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0C92BE11FCD29BA005BA837 /* LifecycleObserver.swift */; }; 26 | F0C92BF71FCD29BA005BA837 /* MutableLiveData.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0C92BE31FCD29BA005BA837 /* MutableLiveData.swift */; }; 27 | F0C92BF81FCD29BA005BA837 /* Transformations.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0C92BE41FCD29BA005BA837 /* Transformations.swift */; }; 28 | F0C92BF91FCD29BA005BA837 /* TransformedLiveData.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0C92BE61FCD29BA005BA837 /* TransformedLiveData.swift */; }; 29 | F0C92BFA1FCD29BA005BA837 /* SwitchedLiveData.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0C92BE71FCD29BA005BA837 /* SwitchedLiveData.swift */; }; 30 | F0C92BFB1FCD29BA005BA837 /* ObserverListController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0C92BE81FCD29BA005BA837 /* ObserverListController.swift */; }; 31 | F0C92BFC1FCD29BA005BA837 /* LiveDataError.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0C92BE91FCD29BA005BA837 /* LiveDataError.swift */; }; 32 | F0C92BFD1FCD29BA005BA837 /* ObserverHandle.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0C92BEA1FCD29BA005BA837 /* ObserverHandle.swift */; }; 33 | F0C92BFE1FCD29BA005BA837 /* ObserverController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0C92BEB1FCD29BA005BA837 /* ObserverController.swift */; }; 34 | F0C92BFF1FCD29BA005BA837 /* MediatorLiveData.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0C92BEC1FCD29BA005BA837 /* MediatorLiveData.swift */; }; 35 | F0C92C001FCD29BA005BA837 /* LiveData.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0C92BED1FCD29BA005BA837 /* LiveData.swift */; }; 36 | /* End PBXBuildFile section */ 37 | 38 | /* Begin PBXContainerItemProxy section */ 39 | F063F7271FCD2AB200B51878 /* PBXContainerItemProxy */ = { 40 | isa = PBXContainerItemProxy; 41 | containerPortal = F0C92BAF1FCD28B7005BA837 /* Project object */; 42 | proxyType = 1; 43 | remoteGlobalIDString = F0C92BB71FCD28B7005BA837; 44 | remoteInfo = ArchitectureComponents; 45 | }; 46 | /* End PBXContainerItemProxy section */ 47 | 48 | /* Begin PBXFileReference section */ 49 | F063F7211FCD2AB100B51878 /* ArchitectureComponentsTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ArchitectureComponentsTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 50 | F063F72C1FCD2AD500B51878 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 51 | F0C92BB81FCD28B7005BA837 /* ArchitectureComponents.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = ArchitectureComponents.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 52 | F0C92BD21FCD29B2005BA837 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 53 | F0C92BD71FCD29BA005BA837 /* LifecycleViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LifecycleViewController.swift; sourceTree = ""; }; 54 | F0C92BD81FCD29BA005BA837 /* LifecycleTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LifecycleTableViewController.swift; sourceTree = ""; }; 55 | F0C92BDA1FCD29BA005BA837 /* LifecycleOwner.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LifecycleOwner.swift; sourceTree = ""; }; 56 | F0C92BDB1FCD29BA005BA837 /* Lifecycle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Lifecycle.swift; sourceTree = ""; }; 57 | F0C92BDD1FCD29BA005BA837 /* Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = ""; }; 58 | F0C92BDE1FCD29BA005BA837 /* LifecycleError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LifecycleError.swift; sourceTree = ""; }; 59 | F0C92BDF1FCD29BA005BA837 /* DefaultLifecycleObserver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultLifecycleObserver.swift; sourceTree = ""; }; 60 | F0C92BE01FCD29BA005BA837 /* LifecycleRegistry.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LifecycleRegistry.swift; sourceTree = ""; }; 61 | F0C92BE11FCD29BA005BA837 /* LifecycleObserver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LifecycleObserver.swift; sourceTree = ""; }; 62 | F0C92BE31FCD29BA005BA837 /* MutableLiveData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MutableLiveData.swift; sourceTree = ""; }; 63 | F0C92BE41FCD29BA005BA837 /* Transformations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Transformations.swift; sourceTree = ""; }; 64 | F0C92BE61FCD29BA005BA837 /* TransformedLiveData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransformedLiveData.swift; sourceTree = ""; }; 65 | F0C92BE71FCD29BA005BA837 /* SwitchedLiveData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwitchedLiveData.swift; sourceTree = ""; }; 66 | F0C92BE81FCD29BA005BA837 /* ObserverListController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObserverListController.swift; sourceTree = ""; }; 67 | F0C92BE91FCD29BA005BA837 /* LiveDataError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LiveDataError.swift; sourceTree = ""; }; 68 | F0C92BEA1FCD29BA005BA837 /* ObserverHandle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObserverHandle.swift; sourceTree = ""; }; 69 | F0C92BEB1FCD29BA005BA837 /* ObserverController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObserverController.swift; sourceTree = ""; }; 70 | F0C92BEC1FCD29BA005BA837 /* MediatorLiveData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediatorLiveData.swift; sourceTree = ""; }; 71 | F0C92BED1FCD29BA005BA837 /* LiveData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LiveData.swift; sourceTree = ""; }; 72 | F0C92C051FCD29C1005BA837 /* LifecycleStateTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LifecycleStateTests.swift; sourceTree = ""; }; 73 | F0C92C071FCD29C1005BA837 /* LiveData_ObserverManagementTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LiveData_ObserverManagementTests.swift; sourceTree = ""; }; 74 | F0C92C081FCD29C1005BA837 /* MutableLiveData_PostValueTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MutableLiveData_PostValueTests.swift; sourceTree = ""; }; 75 | F0C92C091FCD29C1005BA837 /* LiveData_NotificationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LiveData_NotificationTests.swift; sourceTree = ""; }; 76 | F0C92C0B1FCD29C1005BA837 /* MockLifecycleOwner.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockLifecycleOwner.swift; sourceTree = ""; }; 77 | F0C92C0C1FCD29C1005BA837 /* MockLiveDataObserver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockLiveDataObserver.swift; sourceTree = ""; }; 78 | /* End PBXFileReference section */ 79 | 80 | /* Begin PBXFrameworksBuildPhase section */ 81 | F063F71E1FCD2AB100B51878 /* Frameworks */ = { 82 | isa = PBXFrameworksBuildPhase; 83 | buildActionMask = 2147483647; 84 | files = ( 85 | F063F7261FCD2AB200B51878 /* ArchitectureComponents.framework in Frameworks */, 86 | ); 87 | runOnlyForDeploymentPostprocessing = 0; 88 | }; 89 | F0C92BB41FCD28B7005BA837 /* Frameworks */ = { 90 | isa = PBXFrameworksBuildPhase; 91 | buildActionMask = 2147483647; 92 | files = ( 93 | ); 94 | runOnlyForDeploymentPostprocessing = 0; 95 | }; 96 | /* End PBXFrameworksBuildPhase section */ 97 | 98 | /* Begin PBXGroup section */ 99 | F0C92BAE1FCD28B7005BA837 = { 100 | isa = PBXGroup; 101 | children = ( 102 | F0C92BB91FCD28B7005BA837 /* Products */, 103 | F0C92BD41FCD29BA005BA837 /* Sources */, 104 | F0C92C011FCD29C1005BA837 /* Tests */, 105 | ); 106 | sourceTree = ""; 107 | }; 108 | F0C92BB91FCD28B7005BA837 /* Products */ = { 109 | isa = PBXGroup; 110 | children = ( 111 | F0C92BB81FCD28B7005BA837 /* ArchitectureComponents.framework */, 112 | F063F7211FCD2AB100B51878 /* ArchitectureComponentsTests.xctest */, 113 | ); 114 | name = Products; 115 | sourceTree = ""; 116 | }; 117 | F0C92BD41FCD29BA005BA837 /* Sources */ = { 118 | isa = PBXGroup; 119 | children = ( 120 | F0C92BD21FCD29B2005BA837 /* Info.plist */, 121 | F0C92BD51FCD29BA005BA837 /* ArchitectureComponents */, 122 | ); 123 | path = Sources; 124 | sourceTree = ""; 125 | }; 126 | F0C92BD51FCD29BA005BA837 /* ArchitectureComponents */ = { 127 | isa = PBXGroup; 128 | children = ( 129 | F0C92BD91FCD29BA005BA837 /* Lifecycle */, 130 | F0C92BE21FCD29BA005BA837 /* LiveData */, 131 | F0C92BD61FCD29BA005BA837 /* UIKit */, 132 | ); 133 | path = ArchitectureComponents; 134 | sourceTree = ""; 135 | }; 136 | F0C92BD61FCD29BA005BA837 /* UIKit */ = { 137 | isa = PBXGroup; 138 | children = ( 139 | F0C92BD81FCD29BA005BA837 /* LifecycleTableViewController.swift */, 140 | F0C92BD71FCD29BA005BA837 /* LifecycleViewController.swift */, 141 | ); 142 | path = UIKit; 143 | sourceTree = ""; 144 | }; 145 | F0C92BD91FCD29BA005BA837 /* Lifecycle */ = { 146 | isa = PBXGroup; 147 | children = ( 148 | F0C92BDF1FCD29BA005BA837 /* DefaultLifecycleObserver.swift */, 149 | F0C92BDB1FCD29BA005BA837 /* Lifecycle.swift */, 150 | F0C92BE11FCD29BA005BA837 /* LifecycleObserver.swift */, 151 | F0C92BDA1FCD29BA005BA837 /* LifecycleOwner.swift */, 152 | F0C92BE01FCD29BA005BA837 /* LifecycleRegistry.swift */, 153 | F0C92BDC1FCD29BA005BA837 /* Swift */, 154 | ); 155 | path = Lifecycle; 156 | sourceTree = ""; 157 | }; 158 | F0C92BDC1FCD29BA005BA837 /* Swift */ = { 159 | isa = PBXGroup; 160 | children = ( 161 | F0C92BDD1FCD29BA005BA837 /* Extensions.swift */, 162 | F0C92BDE1FCD29BA005BA837 /* LifecycleError.swift */, 163 | ); 164 | path = Swift; 165 | sourceTree = ""; 166 | }; 167 | F0C92BE21FCD29BA005BA837 /* LiveData */ = { 168 | isa = PBXGroup; 169 | children = ( 170 | F0C92BED1FCD29BA005BA837 /* LiveData.swift */, 171 | F0C92BEC1FCD29BA005BA837 /* MediatorLiveData.swift */, 172 | F0C92BE31FCD29BA005BA837 /* MutableLiveData.swift */, 173 | F0C92BE41FCD29BA005BA837 /* Transformations.swift */, 174 | F0C92BE51FCD29BA005BA837 /* Swift */, 175 | ); 176 | path = LiveData; 177 | sourceTree = ""; 178 | }; 179 | F0C92BE51FCD29BA005BA837 /* Swift */ = { 180 | isa = PBXGroup; 181 | children = ( 182 | F0C92BE91FCD29BA005BA837 /* LiveDataError.swift */, 183 | F0C92BEB1FCD29BA005BA837 /* ObserverController.swift */, 184 | F0C92BEA1FCD29BA005BA837 /* ObserverHandle.swift */, 185 | F0C92BE81FCD29BA005BA837 /* ObserverListController.swift */, 186 | F0C92BE71FCD29BA005BA837 /* SwitchedLiveData.swift */, 187 | F0C92BE61FCD29BA005BA837 /* TransformedLiveData.swift */, 188 | ); 189 | path = Swift; 190 | sourceTree = ""; 191 | }; 192 | F0C92C011FCD29C1005BA837 /* Tests */ = { 193 | isa = PBXGroup; 194 | children = ( 195 | F063F72C1FCD2AD500B51878 /* Info.plist */, 196 | F0C92C031FCD29C1005BA837 /* ArchitectureComponentsTests */, 197 | ); 198 | path = Tests; 199 | sourceTree = ""; 200 | }; 201 | F0C92C031FCD29C1005BA837 /* ArchitectureComponentsTests */ = { 202 | isa = PBXGroup; 203 | children = ( 204 | F0C92C041FCD29C1005BA837 /* Lifecycle */, 205 | F0C92C061FCD29C1005BA837 /* LiveData */, 206 | ); 207 | path = ArchitectureComponentsTests; 208 | sourceTree = ""; 209 | }; 210 | F0C92C041FCD29C1005BA837 /* Lifecycle */ = { 211 | isa = PBXGroup; 212 | children = ( 213 | F0C92C051FCD29C1005BA837 /* LifecycleStateTests.swift */, 214 | ); 215 | path = Lifecycle; 216 | sourceTree = ""; 217 | }; 218 | F0C92C061FCD29C1005BA837 /* LiveData */ = { 219 | isa = PBXGroup; 220 | children = ( 221 | F0C92C091FCD29C1005BA837 /* LiveData_NotificationTests.swift */, 222 | F0C92C071FCD29C1005BA837 /* LiveData_ObserverManagementTests.swift */, 223 | F0C92C081FCD29C1005BA837 /* MutableLiveData_PostValueTests.swift */, 224 | F0C92C0A1FCD29C1005BA837 /* Helpers */, 225 | ); 226 | path = LiveData; 227 | sourceTree = ""; 228 | }; 229 | F0C92C0A1FCD29C1005BA837 /* Helpers */ = { 230 | isa = PBXGroup; 231 | children = ( 232 | F0C92C0B1FCD29C1005BA837 /* MockLifecycleOwner.swift */, 233 | F0C92C0C1FCD29C1005BA837 /* MockLiveDataObserver.swift */, 234 | ); 235 | path = Helpers; 236 | sourceTree = ""; 237 | }; 238 | /* End PBXGroup section */ 239 | 240 | /* Begin PBXHeadersBuildPhase section */ 241 | F0C92BB51FCD28B7005BA837 /* Headers */ = { 242 | isa = PBXHeadersBuildPhase; 243 | buildActionMask = 2147483647; 244 | files = ( 245 | ); 246 | runOnlyForDeploymentPostprocessing = 0; 247 | }; 248 | /* End PBXHeadersBuildPhase section */ 249 | 250 | /* Begin PBXNativeTarget section */ 251 | F063F7201FCD2AB100B51878 /* ArchitectureComponentsTests */ = { 252 | isa = PBXNativeTarget; 253 | buildConfigurationList = F063F7291FCD2AB200B51878 /* Build configuration list for PBXNativeTarget "ArchitectureComponentsTests" */; 254 | buildPhases = ( 255 | F063F71D1FCD2AB100B51878 /* Sources */, 256 | F063F71E1FCD2AB100B51878 /* Frameworks */, 257 | F063F71F1FCD2AB100B51878 /* Resources */, 258 | ); 259 | buildRules = ( 260 | ); 261 | dependencies = ( 262 | F063F7281FCD2AB200B51878 /* PBXTargetDependency */, 263 | ); 264 | name = ArchitectureComponentsTests; 265 | productName = ArchitectureComponentsTests; 266 | productReference = F063F7211FCD2AB100B51878 /* ArchitectureComponentsTests.xctest */; 267 | productType = "com.apple.product-type.bundle.unit-test"; 268 | }; 269 | F0C92BB71FCD28B7005BA837 /* ArchitectureComponents */ = { 270 | isa = PBXNativeTarget; 271 | buildConfigurationList = F0C92BCC1FCD28B7005BA837 /* Build configuration list for PBXNativeTarget "ArchitectureComponents" */; 272 | buildPhases = ( 273 | F0C92BB31FCD28B7005BA837 /* Sources */, 274 | F0C92BB41FCD28B7005BA837 /* Frameworks */, 275 | F0C92BB51FCD28B7005BA837 /* Headers */, 276 | F0C92BB61FCD28B7005BA837 /* Resources */, 277 | ); 278 | buildRules = ( 279 | ); 280 | dependencies = ( 281 | ); 282 | name = ArchitectureComponents; 283 | productName = ArchitectureComponents; 284 | productReference = F0C92BB81FCD28B7005BA837 /* ArchitectureComponents.framework */; 285 | productType = "com.apple.product-type.framework"; 286 | }; 287 | /* End PBXNativeTarget section */ 288 | 289 | /* Begin PBXProject section */ 290 | F0C92BAF1FCD28B7005BA837 /* Project object */ = { 291 | isa = PBXProject; 292 | attributes = { 293 | LastSwiftUpdateCheck = 0910; 294 | LastUpgradeCheck = 0910; 295 | ORGANIZATIONNAME = "SPRI LLC"; 296 | TargetAttributes = { 297 | F063F7201FCD2AB100B51878 = { 298 | CreatedOnToolsVersion = 9.1; 299 | ProvisioningStyle = Automatic; 300 | }; 301 | F0C92BB71FCD28B7005BA837 = { 302 | CreatedOnToolsVersion = 9.1; 303 | ProvisioningStyle = Automatic; 304 | }; 305 | }; 306 | }; 307 | buildConfigurationList = F0C92BB21FCD28B7005BA837 /* Build configuration list for PBXProject "ArchitectureComponents" */; 308 | compatibilityVersion = "Xcode 8.0"; 309 | developmentRegion = en; 310 | hasScannedForEncodings = 0; 311 | knownRegions = ( 312 | en, 313 | ); 314 | mainGroup = F0C92BAE1FCD28B7005BA837; 315 | productRefGroup = F0C92BB91FCD28B7005BA837 /* Products */; 316 | projectDirPath = ""; 317 | projectRoot = ""; 318 | targets = ( 319 | F0C92BB71FCD28B7005BA837 /* ArchitectureComponents */, 320 | F063F7201FCD2AB100B51878 /* ArchitectureComponentsTests */, 321 | ); 322 | }; 323 | /* End PBXProject section */ 324 | 325 | /* Begin PBXResourcesBuildPhase section */ 326 | F063F71F1FCD2AB100B51878 /* Resources */ = { 327 | isa = PBXResourcesBuildPhase; 328 | buildActionMask = 2147483647; 329 | files = ( 330 | ); 331 | runOnlyForDeploymentPostprocessing = 0; 332 | }; 333 | F0C92BB61FCD28B7005BA837 /* Resources */ = { 334 | isa = PBXResourcesBuildPhase; 335 | buildActionMask = 2147483647; 336 | files = ( 337 | ); 338 | runOnlyForDeploymentPostprocessing = 0; 339 | }; 340 | /* End PBXResourcesBuildPhase section */ 341 | 342 | /* Begin PBXSourcesBuildPhase section */ 343 | F063F71D1FCD2AB100B51878 /* Sources */ = { 344 | isa = PBXSourcesBuildPhase; 345 | buildActionMask = 2147483647; 346 | files = ( 347 | F063F7311FCD2B1900B51878 /* MutableLiveData_PostValueTests.swift in Sources */, 348 | F063F7331FCD2B1C00B51878 /* MockLiveDataObserver.swift in Sources */, 349 | F063F7321FCD2B1C00B51878 /* MockLifecycleOwner.swift in Sources */, 350 | F063F72F1FCD2B1900B51878 /* LiveData_NotificationTests.swift in Sources */, 351 | F063F72E1FCD2B1500B51878 /* LifecycleStateTests.swift in Sources */, 352 | F063F7301FCD2B1900B51878 /* LiveData_ObserverManagementTests.swift in Sources */, 353 | ); 354 | runOnlyForDeploymentPostprocessing = 0; 355 | }; 356 | F0C92BB31FCD28B7005BA837 /* Sources */ = { 357 | isa = PBXSourcesBuildPhase; 358 | buildActionMask = 2147483647; 359 | files = ( 360 | F0C92BF21FCD29BA005BA837 /* Extensions.swift in Sources */, 361 | F0C92BFE1FCD29BA005BA837 /* ObserverController.swift in Sources */, 362 | F0C92BF01FCD29BA005BA837 /* LifecycleOwner.swift in Sources */, 363 | F0C92BEF1FCD29BA005BA837 /* LifecycleTableViewController.swift in Sources */, 364 | F0C92BFD1FCD29BA005BA837 /* ObserverHandle.swift in Sources */, 365 | F0C92BF91FCD29BA005BA837 /* TransformedLiveData.swift in Sources */, 366 | F0C92BFA1FCD29BA005BA837 /* SwitchedLiveData.swift in Sources */, 367 | F0C92BFB1FCD29BA005BA837 /* ObserverListController.swift in Sources */, 368 | F0C92BF51FCD29BA005BA837 /* LifecycleRegistry.swift in Sources */, 369 | F0C92BF81FCD29BA005BA837 /* Transformations.swift in Sources */, 370 | F0C92BF11FCD29BA005BA837 /* Lifecycle.swift in Sources */, 371 | F0C92BEE1FCD29BA005BA837 /* LifecycleViewController.swift in Sources */, 372 | F0C92BFF1FCD29BA005BA837 /* MediatorLiveData.swift in Sources */, 373 | F0C92BFC1FCD29BA005BA837 /* LiveDataError.swift in Sources */, 374 | F0C92BF41FCD29BA005BA837 /* DefaultLifecycleObserver.swift in Sources */, 375 | F0C92BF61FCD29BA005BA837 /* LifecycleObserver.swift in Sources */, 376 | F0C92BF71FCD29BA005BA837 /* MutableLiveData.swift in Sources */, 377 | F0C92BF31FCD29BA005BA837 /* LifecycleError.swift in Sources */, 378 | F0C92C001FCD29BA005BA837 /* LiveData.swift in Sources */, 379 | ); 380 | runOnlyForDeploymentPostprocessing = 0; 381 | }; 382 | /* End PBXSourcesBuildPhase section */ 383 | 384 | /* Begin PBXTargetDependency section */ 385 | F063F7281FCD2AB200B51878 /* PBXTargetDependency */ = { 386 | isa = PBXTargetDependency; 387 | target = F0C92BB71FCD28B7005BA837 /* ArchitectureComponents */; 388 | targetProxy = F063F7271FCD2AB200B51878 /* PBXContainerItemProxy */; 389 | }; 390 | /* End PBXTargetDependency section */ 391 | 392 | /* Begin XCBuildConfiguration section */ 393 | F063F72A1FCD2AB200B51878 /* Debug */ = { 394 | isa = XCBuildConfiguration; 395 | buildSettings = { 396 | CODE_SIGN_STYLE = Automatic; 397 | DEVELOPMENT_TEAM = 2D477K74X5; 398 | INFOPLIST_FILE = ArchitectureComponentsTests/Info.plist; 399 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 400 | PRODUCT_BUNDLE_IDENTIFIER = com.spr.ArchitectureComponentsTests; 401 | PRODUCT_NAME = "$(TARGET_NAME)"; 402 | SWIFT_VERSION = 4.0; 403 | TARGETED_DEVICE_FAMILY = "1,2"; 404 | }; 405 | name = Debug; 406 | }; 407 | F063F72B1FCD2AB200B51878 /* Release */ = { 408 | isa = XCBuildConfiguration; 409 | buildSettings = { 410 | CODE_SIGN_STYLE = Automatic; 411 | DEVELOPMENT_TEAM = 2D477K74X5; 412 | INFOPLIST_FILE = ArchitectureComponentsTests/Info.plist; 413 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 414 | PRODUCT_BUNDLE_IDENTIFIER = com.spr.ArchitectureComponentsTests; 415 | PRODUCT_NAME = "$(TARGET_NAME)"; 416 | SWIFT_VERSION = 4.0; 417 | TARGETED_DEVICE_FAMILY = "1,2"; 418 | }; 419 | name = Release; 420 | }; 421 | F0C92BCA1FCD28B7005BA837 /* Debug */ = { 422 | isa = XCBuildConfiguration; 423 | buildSettings = { 424 | ALWAYS_SEARCH_USER_PATHS = NO; 425 | CLANG_ANALYZER_NONNULL = YES; 426 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 427 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 428 | CLANG_CXX_LIBRARY = "libc++"; 429 | CLANG_ENABLE_MODULES = YES; 430 | CLANG_ENABLE_OBJC_ARC = YES; 431 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 432 | CLANG_WARN_BOOL_CONVERSION = YES; 433 | CLANG_WARN_COMMA = YES; 434 | CLANG_WARN_CONSTANT_CONVERSION = YES; 435 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 436 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 437 | CLANG_WARN_EMPTY_BODY = YES; 438 | CLANG_WARN_ENUM_CONVERSION = YES; 439 | CLANG_WARN_INFINITE_RECURSION = YES; 440 | CLANG_WARN_INT_CONVERSION = YES; 441 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 442 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 443 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 444 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 445 | CLANG_WARN_STRICT_PROTOTYPES = YES; 446 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 447 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 448 | CLANG_WARN_UNREACHABLE_CODE = YES; 449 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 450 | CODE_SIGN_IDENTITY = "iPhone Developer"; 451 | COPY_PHASE_STRIP = NO; 452 | CURRENT_PROJECT_VERSION = 1; 453 | DEBUG_INFORMATION_FORMAT = dwarf; 454 | ENABLE_STRICT_OBJC_MSGSEND = YES; 455 | ENABLE_TESTABILITY = YES; 456 | GCC_C_LANGUAGE_STANDARD = gnu11; 457 | GCC_DYNAMIC_NO_PIC = NO; 458 | GCC_NO_COMMON_BLOCKS = YES; 459 | GCC_OPTIMIZATION_LEVEL = 0; 460 | GCC_PREPROCESSOR_DEFINITIONS = ( 461 | "DEBUG=1", 462 | "$(inherited)", 463 | ); 464 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 465 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 466 | GCC_WARN_UNDECLARED_SELECTOR = YES; 467 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 468 | GCC_WARN_UNUSED_FUNCTION = YES; 469 | GCC_WARN_UNUSED_VARIABLE = YES; 470 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 471 | MTL_ENABLE_DEBUG_INFO = YES; 472 | ONLY_ACTIVE_ARCH = YES; 473 | SDKROOT = iphoneos; 474 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 475 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 476 | VERSIONING_SYSTEM = "apple-generic"; 477 | VERSION_INFO_PREFIX = ""; 478 | }; 479 | name = Debug; 480 | }; 481 | F0C92BCB1FCD28B7005BA837 /* Release */ = { 482 | isa = XCBuildConfiguration; 483 | buildSettings = { 484 | ALWAYS_SEARCH_USER_PATHS = NO; 485 | CLANG_ANALYZER_NONNULL = YES; 486 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 487 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 488 | CLANG_CXX_LIBRARY = "libc++"; 489 | CLANG_ENABLE_MODULES = YES; 490 | CLANG_ENABLE_OBJC_ARC = YES; 491 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 492 | CLANG_WARN_BOOL_CONVERSION = YES; 493 | CLANG_WARN_COMMA = YES; 494 | CLANG_WARN_CONSTANT_CONVERSION = YES; 495 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 496 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 497 | CLANG_WARN_EMPTY_BODY = YES; 498 | CLANG_WARN_ENUM_CONVERSION = YES; 499 | CLANG_WARN_INFINITE_RECURSION = YES; 500 | CLANG_WARN_INT_CONVERSION = YES; 501 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 502 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 503 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 504 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 505 | CLANG_WARN_STRICT_PROTOTYPES = YES; 506 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 507 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 508 | CLANG_WARN_UNREACHABLE_CODE = YES; 509 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 510 | CODE_SIGN_IDENTITY = "iPhone Developer"; 511 | COPY_PHASE_STRIP = NO; 512 | CURRENT_PROJECT_VERSION = 1; 513 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 514 | ENABLE_NS_ASSERTIONS = NO; 515 | ENABLE_STRICT_OBJC_MSGSEND = YES; 516 | GCC_C_LANGUAGE_STANDARD = gnu11; 517 | GCC_NO_COMMON_BLOCKS = YES; 518 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 519 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 520 | GCC_WARN_UNDECLARED_SELECTOR = YES; 521 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 522 | GCC_WARN_UNUSED_FUNCTION = YES; 523 | GCC_WARN_UNUSED_VARIABLE = YES; 524 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 525 | MTL_ENABLE_DEBUG_INFO = NO; 526 | SDKROOT = iphoneos; 527 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 528 | VALIDATE_PRODUCT = YES; 529 | VERSIONING_SYSTEM = "apple-generic"; 530 | VERSION_INFO_PREFIX = ""; 531 | }; 532 | name = Release; 533 | }; 534 | F0C92BCD1FCD28B7005BA837 /* Debug */ = { 535 | isa = XCBuildConfiguration; 536 | buildSettings = { 537 | CODE_SIGN_IDENTITY = ""; 538 | CODE_SIGN_STYLE = Automatic; 539 | DEFINES_MODULE = YES; 540 | DEVELOPMENT_TEAM = 2D477K74X5; 541 | DYLIB_COMPATIBILITY_VERSION = 1; 542 | DYLIB_CURRENT_VERSION = 1; 543 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 544 | INFOPLIST_FILE = "$(SRCROOT)/Sources/Info.plist"; 545 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 546 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 547 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 548 | PRODUCT_BUNDLE_IDENTIFIER = com.spr.ArchitectureComponents; 549 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 550 | SKIP_INSTALL = YES; 551 | SWIFT_VERSION = 4.0; 552 | TARGETED_DEVICE_FAMILY = "1,2"; 553 | }; 554 | name = Debug; 555 | }; 556 | F0C92BCE1FCD28B7005BA837 /* Release */ = { 557 | isa = XCBuildConfiguration; 558 | buildSettings = { 559 | CODE_SIGN_IDENTITY = ""; 560 | CODE_SIGN_STYLE = Automatic; 561 | DEFINES_MODULE = YES; 562 | DEVELOPMENT_TEAM = 2D477K74X5; 563 | DYLIB_COMPATIBILITY_VERSION = 1; 564 | DYLIB_CURRENT_VERSION = 1; 565 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 566 | INFOPLIST_FILE = "$(SRCROOT)/Sources/Info.plist"; 567 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 568 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 569 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 570 | PRODUCT_BUNDLE_IDENTIFIER = com.spr.ArchitectureComponents; 571 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 572 | SKIP_INSTALL = YES; 573 | SWIFT_VERSION = 4.0; 574 | TARGETED_DEVICE_FAMILY = "1,2"; 575 | }; 576 | name = Release; 577 | }; 578 | /* End XCBuildConfiguration section */ 579 | 580 | /* Begin XCConfigurationList section */ 581 | F063F7291FCD2AB200B51878 /* Build configuration list for PBXNativeTarget "ArchitectureComponentsTests" */ = { 582 | isa = XCConfigurationList; 583 | buildConfigurations = ( 584 | F063F72A1FCD2AB200B51878 /* Debug */, 585 | F063F72B1FCD2AB200B51878 /* Release */, 586 | ); 587 | defaultConfigurationIsVisible = 0; 588 | defaultConfigurationName = Release; 589 | }; 590 | F0C92BB21FCD28B7005BA837 /* Build configuration list for PBXProject "ArchitectureComponents" */ = { 591 | isa = XCConfigurationList; 592 | buildConfigurations = ( 593 | F0C92BCA1FCD28B7005BA837 /* Debug */, 594 | F0C92BCB1FCD28B7005BA837 /* Release */, 595 | ); 596 | defaultConfigurationIsVisible = 0; 597 | defaultConfigurationName = Release; 598 | }; 599 | F0C92BCC1FCD28B7005BA837 /* Build configuration list for PBXNativeTarget "ArchitectureComponents" */ = { 600 | isa = XCConfigurationList; 601 | buildConfigurations = ( 602 | F0C92BCD1FCD28B7005BA837 /* Debug */, 603 | F0C92BCE1FCD28B7005BA837 /* Release */, 604 | ); 605 | defaultConfigurationIsVisible = 0; 606 | defaultConfigurationName = Release; 607 | }; 608 | /* End XCConfigurationList section */ 609 | }; 610 | rootObject = F0C92BAF1FCD28B7005BA837 /* Project object */; 611 | } 612 | -------------------------------------------------------------------------------- /ArchitectureComponents.xcodeproj/xcshareddata/xcschemes/ArchitectureComponents.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 34 | 40 | 41 | 42 | 43 | 44 | 50 | 51 | 52 | 53 | 54 | 55 | 66 | 67 | 73 | 74 | 75 | 76 | 77 | 78 | 84 | 85 | 91 | 92 | 93 | 94 | 96 | 97 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /ArchitectureComponents.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /CODEOFCONDUCT.md: -------------------------------------------------------------------------------- 1 | # SPR Open Source Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at [opensource@spr.com][reportemail]. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | [reportemail]: mailto:opensource@spr.com 75 | -------------------------------------------------------------------------------- /Cartfile: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spropensource/ArchitectureComponents/ee79f060ccbcac69a479af973d3a33426becfa26/Cartfile -------------------------------------------------------------------------------- /Cartfile.private: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spropensource/ArchitectureComponents/ee79f060ccbcac69a479af973d3a33426becfa26/Cartfile.private -------------------------------------------------------------------------------- /Cartfile.resolved: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spropensource/ArchitectureComponents/ee79f060ccbcac69a479af973d3a33426becfa26/Cartfile.resolved -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:4.0 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "ArchitectureComponents", 8 | products: [ 9 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 10 | .library( 11 | name: "ArchitectureComponents", 12 | targets: ["ArchitectureComponents"]), 13 | ], 14 | dependencies: [ 15 | // Dependencies declare other packages that this package depends on. 16 | // .package(url: /* package url */, from: "1.0.0"), 17 | ], 18 | targets: [ 19 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 20 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 21 | .target( 22 | name: "ArchitectureComponents", 23 | dependencies: []), 24 | .testTarget( 25 | name: "ArchitectureComponentsTests", 26 | dependencies: ["ArchitectureComponents"]), 27 | ] 28 | ) 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Architecture Components 2 | 3 | A port of Android Architecture Components to iOS. 4 | 5 | 6 | ## Motivation 7 | 8 | When building an app for both iOS and Android, there are advantages to defining 9 | a single app architecture and applying it on both platforms. Primarily, it 10 | saves time by solving the data modeling and component interactions once for 11 | both platforms. Additionally, maintenance is somewhat eased because changes 12 | will be similar on both platforms. 13 | 14 | Apple's developer guides and the UIKit framework provide little concrete 15 | guidance for architecting an app to be reliable and maintainable. This has lead 16 | the iOS developer community to create [many iOS app architectures][MVX] and 17 | have [lively debates][MCHADO] about their usefulness. 18 | 19 | [Android Architecture Components][AAC] provide Android developers with a 20 | general-purpose, clean, repeatable pattern for creating data-driven, 21 | reactive-style apps. The components provide a clean separation of concerns and 22 | abstract away many of the intricacies that arise when handling data updates 23 | properly for every phase of the UI lifecycle. Finally, Google provides 24 | [concrete recommendations][GUIDE] for common real-world app development 25 | problems. 26 | 27 | Since iOS lacks a first-party application architecture and Android now has 28 | a very nice first-party application architecture, it seems reasonable to adopt 29 | Android's architecture on both platforms in instances when app delivery would 30 | benefit from sharing an app architecture. 31 | 32 | 33 | ## Roadmap 34 | 35 | Next steps: 36 | 37 | - Improve automated test coverage 38 | - Provide full documentation comments 39 | - Improve README with getting started information 40 | - Create an example app 41 | - Setup continuous integration 42 | - Add logging 43 | - Implement Room Persistence Library 44 | - Implement Paging Library 45 | - Support tvOS, watchOS, macOS 46 | 47 | Completed: 48 | 49 | - Implement Lifecycle components 50 | - Implement LiveData components 51 | - Enable Carthage and CocoaPods installation 52 | 53 | 54 | ## Code of Conduct 55 | 56 | Participation in this open source project is governed by the SPR Open Source 57 | Code of Conduct, which outlines expectations for participation in SPR-managed 58 | open source communities and steps for reporting unacceptable behavior. We are 59 | committed to providing a welcoming and inspiring community for all. People 60 | violating this code of conduct may be banned from the community. 61 | 62 | See the `CODEOFCONDUCT.md` file for the full code of conduct. 63 | 64 | 65 | ## License 66 | 67 | This open source project is licensed under the terms of the [Apache 2.0 68 | license][APACHE]. See the `LICENSE` file. Additional, non-authoritative 69 | information about the license can be found at [Choose a License][CHOOSE], [Open 70 | Source Initiative][OSINIT], and [TLDRLegal][TLDR]. 71 | 72 | 73 | 74 | [AAC]: https://developer.android.com/topic/libraries/architecture/index.html 75 | [APACHE]: https://opensource.org/licenses/Apache-2.0 76 | [CHOOSE]: https://choosealicense.com/licenses/apache-2.0/ 77 | [GUIDE]: https://developer.android.com/topic/libraries/architecture/guide.html 78 | [MCHADO]: http://aplus.rs/2017/much-ado-about-ios-app-architecture/ 79 | [MVX]: https://medium.com/ios-os-x-development/ios-architecture-patterns-ecba4c38de52 80 | [OSINIT]: https://opensource.org/licenses/Apache-2.0 81 | [TLDR]: https://tldrlegal.com/license/apache-license-2.0-%28apache-2.0%29 82 | -------------------------------------------------------------------------------- /Sources/ArchitectureComponents/Lifecycle/DefaultLifecycleObserver.swift: -------------------------------------------------------------------------------- 1 | // ArchitectureComponents 2 | // Copyright (c) 2017 SPRI, LLC . Some rights reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | import Foundation 17 | 18 | 19 | open class DefaultLifecycleObserver: LifecycleObserver { 20 | 21 | open func onCreate(owner: LifecycleOwner) { } 22 | 23 | open func onStart(owner: LifecycleOwner) { } 24 | 25 | open func onResume(owner: LifecycleOwner) { } 26 | 27 | open func onPause(owner: LifecycleOwner) { } 28 | 29 | open func onStop(owner: LifecycleOwner) { } 30 | 31 | open func onDestroy(owner: LifecycleOwner) { } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /Sources/ArchitectureComponents/Lifecycle/Lifecycle.swift: -------------------------------------------------------------------------------- 1 | // ArchitectureComponents 2 | // Copyright (c) 2017 SPRI, LLC . Some rights reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | import Foundation 17 | 18 | 19 | /// Defines an object that has an Android Lifecycle. 20 | /// 21 | /// A `Lifecycle` has the following states and events: 22 | /// 23 | /// initialized destroyed created started resumed 24 | /// | | | | | 25 | /// |-------------create----------->| | | 26 | /// | | |----start--->| | 27 | /// | | | |---resume--->| 28 | /// | | | | | 29 | /// | | | | | 30 | /// | | | |<---pause----| 31 | /// | | |<----stop----| | 32 | /// | |<---destroy---| | | 33 | /// | | | | | 34 | public protocol Lifecycle { 35 | 36 | var currentState: LifecycleState { get } 37 | 38 | /// Adds a `LifecycleObserver` that will be notified when the 39 | /// `LifecycleOwner` changes state. 40 | /// 41 | /// Attempting to add the same observer multiple times will not cause the 42 | /// observer to be notified multiple times when the `LifecycleOwner` 43 | /// changes state 44 | func addObserver(_ observer: LifecycleObserver) 45 | 46 | /// Removes the provided observer from the observers list. The observer 47 | /// will no longer be notified when the `LifecycleOwner` changes state. 48 | /// 49 | /// Attempting to remove an observer that is not in the observers list 50 | /// does nothing. 51 | func removeObserver(_ observer: LifecycleObserver) 52 | 53 | } 54 | 55 | public enum LifecycleEvent { 56 | 57 | case create 58 | case start 59 | case resume 60 | case pause 61 | case stop 62 | case destroy 63 | 64 | // MARK: Types 65 | 66 | public typealias Handler = (LifecycleEvent) -> Void 67 | 68 | } 69 | 70 | public enum LifecycleState: Int { 71 | 72 | case initialized = 0 73 | case created = 2 74 | case started = 3 75 | case resumed = 4 76 | case destroyed = 1 77 | 78 | } 79 | -------------------------------------------------------------------------------- /Sources/ArchitectureComponents/Lifecycle/LifecycleObserver.swift: -------------------------------------------------------------------------------- 1 | // ArchitectureComponents 2 | // Copyright (c) 2017 SPRI, LLC . Some rights reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | import Foundation 17 | 18 | 19 | /// Interface for receiving updates about changes to the state of a 20 | /// `LifecycleOwner`. 21 | /// 22 | /// The relationship between `LifecycleState` changes and observer callbacks: 23 | /// 24 | /// initialized destroyed created started resumed 25 | /// | | | | | 26 | /// |-----------onCreate()--------->| | | 27 | /// | | |--onStart()->| | 28 | /// | | | |-onRresume()>| 29 | /// | | | | | 30 | /// | | | | | 31 | /// | | | |<-onPause()--| 32 | /// | | |<--onStop()--| | 33 | /// | |<-onDestroy()-| | | 34 | /// | | | | | 35 | public protocol LifecycleObserver: class { 36 | 37 | /// `LifecycleOwner` transitioned from INITIALIZED to CREATED 38 | func onCreate(owner: LifecycleOwner) 39 | 40 | /// `LifecycleOwner` transitioned from CREATED to STARTED 41 | func onStart(owner: LifecycleOwner) 42 | 43 | /// `LifecycleOwner` transitioned from STARTED to RESUMED 44 | func onResume(owner: LifecycleOwner) 45 | 46 | /// `LifecycleOwner` transitioned from RESUMED to STARTED 47 | func onPause(owner: LifecycleOwner) 48 | 49 | /// `LifecycleOwner` transitioned from STARTED to CREATED 50 | func onStop(owner: LifecycleOwner) 51 | 52 | /// `LifecycleOwner` transitioned from CREATED to DESTROYED 53 | func onDestroy(owner: LifecycleOwner) 54 | 55 | } 56 | -------------------------------------------------------------------------------- /Sources/ArchitectureComponents/Lifecycle/LifecycleOwner.swift: -------------------------------------------------------------------------------- 1 | // ArchitectureComponents 2 | // Copyright (c) 2017 SPRI, LLC . Some rights reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | import Foundation 17 | 18 | 19 | /// A class that has an Android lifecycle. These events can be used by custom 20 | /// components to handle lifecycle changes without implementing any code 21 | /// inside the `UIViewController`. 22 | public protocol LifecycleOwner: class { 23 | 24 | /// The `Lifecycle` of the provider. 25 | var lifecycle: Lifecycle { get } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /Sources/ArchitectureComponents/Lifecycle/LifecycleRegistry.swift: -------------------------------------------------------------------------------- 1 | // ArchitectureComponents 2 | // Copyright (c) 2017 SPRI, LLC . Some rights reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | import Foundation 17 | 18 | 19 | /// Implementation of `Lifecycle` that allows the `LifecycleOwner` to notify 20 | /// observers of state changes. 21 | public class LifecycleRegistry: Lifecycle { 22 | // TODO: write tests for this class 23 | 24 | // MARK: Types 25 | 26 | /// Envelope that holds a weak reference to a `LifecycleObserver` and provides 27 | /// additional capabilities useful to `LifecycleRegistry`. 28 | private class LifecycleObserverEnvelope: Equatable, Hashable { 29 | 30 | // MARK: Equatable 31 | 32 | public static func ==(lhs: LifecycleObserverEnvelope, rhs: LifecycleObserverEnvelope) -> Bool { 33 | return (lhs.lifecycleObserver === rhs.lifecycleObserver) 34 | } 35 | 36 | // MARK: Properties 37 | 38 | public private(set) weak var lifecycleObserver: LifecycleObserver? 39 | 40 | // MARK: init / deinit 41 | 42 | public init(observer: LifecycleObserver) { 43 | self.lifecycleObserver = observer 44 | } 45 | 46 | // MARK: Hashable 47 | 48 | public var hashValue: Int { 49 | guard let observer = self.lifecycleObserver else { return 0 } 50 | 51 | let hashed = ObjectIdentifier(observer).hashValue 52 | return hashed 53 | } 54 | 55 | } 56 | 57 | // MARK: Properties 58 | 59 | // It is possible for another object to hold a reference to this Lifecycle 60 | // long after this Lifecycle's owner has been disposed. Therefore, we need 61 | // a weak reference, not an unowned reference. 62 | private weak var owner: LifecycleOwner? 63 | 64 | private var weakObservers: Set = [] 65 | 66 | // MARK: Init / Deinit 67 | 68 | public convenience init(provider: LifecycleOwner) { 69 | self.init(provider: provider, initialState: .initialized) 70 | } 71 | 72 | public init(provider: LifecycleOwner, initialState: LifecycleState) { 73 | self.currentState = initialState 74 | self.owner = provider 75 | } 76 | 77 | // MARK: Lifecycle 78 | 79 | public private(set) var currentState: LifecycleState 80 | 81 | public func addObserver(_ observer: LifecycleObserver) { 82 | assert(Thread.isMainThread) 83 | 84 | // take the opportunity to remove any observers that have been 85 | // deallocated 86 | self.cleanupObservers() 87 | 88 | // add the provided observer to the list of observers 89 | let weakObserver = LifecycleObserverEnvelope(observer: observer) 90 | self.weakObservers.insert(weakObserver) 91 | } 92 | 93 | /// Thread-safe, since it common for this method to be called from a 94 | /// type's `deinit`, which can be called from any thread. 95 | public func removeObserver(_ observer: LifecycleObserver) { 96 | let runIt = { 97 | // take the opportunity to remove any observers that have been 98 | // deallocated 99 | self.cleanupObservers() 100 | 101 | // remove the provided observer from the list of observers 102 | let weakObserver = LifecycleObserverEnvelope(observer: observer) 103 | self.weakObservers.remove(weakObserver) 104 | } 105 | 106 | if Thread.isMainThread { 107 | runIt() 108 | } else { 109 | DispatchQueue.main.async(execute: runIt) 110 | } 111 | } 112 | 113 | // MARK: LifecycleRegistry 114 | 115 | public var observerCount: Int { 116 | assert(Thread.isMainThread) 117 | 118 | cleanupObservers() 119 | 120 | let count = self.weakObservers.count 121 | return count 122 | } 123 | 124 | public func handleLifecycleEvent(_ event: LifecycleEvent, beforeObservers handler: LifecycleEvent.Handler? = nil) { 125 | assert(Thread.isMainThread) 126 | 127 | // Update properties FIRST, THEN notify everyone of the change. 128 | self.currentState = event.stateAfter 129 | 130 | // Notify event handler BEFORE notifying observers. This is used by 131 | // LifecycleViewController to call its own onCreate(), onStart(), etc. 132 | // before the observers start doing their work. 133 | if let handler = handler { 134 | handler(event) 135 | } 136 | 137 | // Notify the observers 138 | self.notifyObserversOfEvent(event) 139 | } 140 | 141 | public func markState(_ targetState: LifecycleState, beforeObservers handler: LifecycleEvent.Handler? = nil) { 142 | assert(Thread.isMainThread) 143 | 144 | // determine the steps required to transition from current state to the 145 | // target state 146 | guard let events = try? self.currentState.eventsToState(targetState) else { return } 147 | 148 | // update the lifecycle's state to each state required to transition 149 | // from the current state to the target state 150 | for event in events { 151 | self.handleLifecycleEvent(event, beforeObservers: handler) 152 | } 153 | } 154 | 155 | // MARK: Private (Observers) 156 | 157 | private func cleanupObservers() { 158 | let remainingObservers = self.weakObservers.filter({ $0.lifecycleObserver != nil }) 159 | self.weakObservers = remainingObservers 160 | } 161 | 162 | private func notifyObserversOfEvent(_ event: LifecycleEvent) { 163 | guard let owner = self.owner else { return } 164 | 165 | let activeObservers = self.weakObservers.flatMap({ $0.lifecycleObserver }) 166 | for observer in activeObservers { 167 | switch event { 168 | case .create: observer.onCreate(owner: owner) 169 | case .start: observer.onStart(owner: owner) 170 | case .resume: observer.onResume(owner: owner) 171 | case .pause: observer.onPause(owner: owner) 172 | case .stop: observer.onStop(owner: owner) 173 | case .destroy: observer.onDestroy(owner: owner) 174 | } 175 | } 176 | } 177 | 178 | } 179 | -------------------------------------------------------------------------------- /Sources/ArchitectureComponents/Lifecycle/Swift/Extensions.swift: -------------------------------------------------------------------------------- 1 | // ArchitectureComponents 2 | // Copyright (c) 2017 SPRI, LLC . Some rights reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | import Foundation 17 | 18 | 19 | extension LifecycleError: CustomStringConvertible { 20 | 21 | public var description: String { 22 | let string: String 23 | switch self { 24 | case let .invalidStateTransition(from: f, to: t): 25 | string = "Invalid state transition from \(f) to \(t)" 26 | case let .invalidEventForState(state: s, event: e): 27 | string = "Invalid event \(e) while in state \(s)" 28 | } 29 | return string 30 | } 31 | 32 | } 33 | 34 | 35 | extension LifecycleEvent: CustomStringConvertible { 36 | 37 | // MARK: CustomStringConvertible 38 | 39 | public var description: String { 40 | let string: String 41 | 42 | switch self { 43 | case .create: string = "CREATE" 44 | case .start: string = "START" 45 | case .resume: string = "RESUME" 46 | case .pause: string = "PAUSE" 47 | case .stop: string = "STOP" 48 | case .destroy: string = "DESTROY" 49 | } 50 | 51 | return string 52 | } 53 | 54 | } 55 | 56 | internal extension LifecycleEvent { 57 | 58 | // MARK: Helpers 59 | 60 | internal var stateAfter: LifecycleState { 61 | let state: LifecycleState 62 | switch self { 63 | case .create: state = .created 64 | case .start: state = .started 65 | case .resume: state = .resumed 66 | case .pause: state = .started 67 | case .stop: state = .created 68 | case .destroy: state = .destroyed 69 | } 70 | return state 71 | } 72 | 73 | internal var stateBefore: LifecycleState { 74 | let state: LifecycleState 75 | switch self { 76 | case .create: state = .initialized 77 | case .start: state = .created 78 | case .resume: state = .started 79 | case .pause: state = .resumed 80 | case .stop: state = .started 81 | case .destroy: state = .created 82 | } 83 | return state 84 | } 85 | 86 | } 87 | 88 | extension LifecycleState: Comparable, CustomStringConvertible { 89 | 90 | public static func <(lhs: LifecycleState, rhs: LifecycleState) -> Bool { 91 | let comp = lhs.rawValue < rhs.rawValue 92 | return comp 93 | } 94 | 95 | public var description: String { 96 | let string: String 97 | switch self { 98 | case .initialized: string = "INITIALIZED" 99 | case .created: string = "CREATED" 100 | case .started: string = "STARTED" 101 | case .resumed: string = "RESUMED" 102 | case .destroyed: string = "DESTROYED" 103 | } 104 | return string 105 | } 106 | } 107 | 108 | internal extension LifecycleState { 109 | 110 | internal func eventsToState(_ targetState: LifecycleState) throws -> [LifecycleEvent] { 111 | guard self != targetState else { return [] } 112 | guard targetState != .initialized else { 113 | throw LifecycleError.invalidStateTransition(from: self, to: targetState) 114 | } 115 | 116 | let event: LifecycleEvent? 117 | if self < targetState { 118 | event = self.eventToAdvanceState 119 | } else { 120 | event = self.eventToRetreatState 121 | } 122 | 123 | guard let nextEvent = event else { 124 | throw LifecycleError.invalidStateTransition(from: self, to: targetState) 125 | } 126 | 127 | let trailingEvents = try nextEvent.stateAfter.eventsToState(targetState) 128 | let events = [nextEvent] + trailingEvents // TODO: maybe use .insert(_:at:) ? 129 | return events 130 | } 131 | 132 | private var eventToAdvanceState: LifecycleEvent? { 133 | let event: LifecycleEvent? 134 | switch self { 135 | case .initialized: event = .create 136 | case .created: event = .start 137 | case .started: event = .resume 138 | default: event = nil 139 | } 140 | return event 141 | } 142 | 143 | private var eventToRetreatState: LifecycleEvent? { 144 | let event: LifecycleEvent? 145 | switch self { 146 | case .created: event = .destroy 147 | case .started: event = .stop 148 | case .resumed: event = .pause 149 | default: event = nil 150 | } 151 | return event 152 | } 153 | 154 | } 155 | -------------------------------------------------------------------------------- /Sources/ArchitectureComponents/Lifecycle/Swift/LifecycleError.swift: -------------------------------------------------------------------------------- 1 | // ArchitectureComponents 2 | // Copyright (c) 2017 SPRI, LLC . Some rights reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | 17 | public enum LifecycleError: Swift.Error { 18 | case invalidEventForState(state: LifecycleState, event: LifecycleEvent) 19 | case invalidStateTransition(from: LifecycleState, to: LifecycleState) 20 | } 21 | -------------------------------------------------------------------------------- /Sources/ArchitectureComponents/LiveData/LiveData.swift: -------------------------------------------------------------------------------- 1 | // ArchitectureComponents 2 | // Copyright (c) 2017 SPRI, LLC . Some rights reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | import Foundation 17 | 18 | 19 | /// Generally, `LiveData` delivers updates only when data changes, and only to 20 | /// active observers. An exception to this behavior is that observers also 21 | /// receive an update when they change from an inactive to an active state. 22 | /// Furthermore, if the observer changes from inactive to active a second time, 23 | /// it only receives an update if the value has changed since the last time it 24 | /// became active. 25 | open class LiveData { 26 | 27 | // MARK: Types 28 | 29 | public typealias Observer = (Type) -> Void 30 | 31 | // MARK: Properties 32 | 33 | public internal(set) var value: Type 34 | 35 | // MARK: Init / Deinit 36 | 37 | public init(initialValue: Type) { 38 | self.value = initialValue 39 | } 40 | 41 | // MARK: LiveData 42 | 43 | public var hasObservers: Bool { 44 | return false 45 | } 46 | 47 | public var hasActiveObservers: Bool { 48 | return false 49 | } 50 | 51 | public func observe(owner: LifecycleOwner, observer: @escaping Observer) -> ObserverHandle { 52 | fatalError("LiveData.observe(owner:observer:) should have override") 53 | } 54 | 55 | public func observeForever(observer: @escaping Observer) -> ObserverHandle { 56 | fatalError("LiveData.observeForever(observer:) should have override") 57 | } 58 | 59 | public func removeObserver(handle: ObserverHandle) { 60 | fatalError("LiveData.removeObserver(handle:) should have override") 61 | } 62 | 63 | public func removeObservers(owner: LifecycleOwner) { 64 | fatalError("LiveData.removeObservers(owner:) should have override") 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /Sources/ArchitectureComponents/LiveData/MediatorLiveData.swift: -------------------------------------------------------------------------------- 1 | // ArchitectureComponents 2 | // Copyright (c) 2017 SPRI, LLC . Some rights reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | import Foundation 17 | 18 | 19 | open class MediatorLiveData: MutableLiveData { 20 | // TODO: write tests for this class 21 | 22 | // MARK: Properties 23 | 24 | private var active: Bool = false 25 | private var handlesBySource: [ObjectIdentifier: ObserverHandle] = [:] 26 | 27 | // MARK: LiveData 28 | 29 | open override func onActive() { 30 | self.active = true 31 | } 32 | 33 | open override func onInactive() { 34 | self.active = false 35 | } 36 | 37 | // MARK: MutableLiveData (Public) 38 | 39 | public func addSource(_ source: LiveData, observer: @escaping Observer) throws { 40 | let sourceIdentifier = ObjectIdentifier(source) 41 | guard self.handlesBySource[sourceIdentifier] == nil else { 42 | throw LiveDataError.sourceAlreadyAddedToMediator 43 | } 44 | 45 | let handle = source.observeForever { [weak self] (value) in 46 | guard 47 | let active = self?.active, 48 | active 49 | else { return } 50 | 51 | observer(value) 52 | } 53 | 54 | self.handlesBySource[sourceIdentifier] = handle 55 | } 56 | 57 | public func removeSource(_ source: LiveData) { 58 | let sourceIdentifier = ObjectIdentifier(source) 59 | guard let handle = self.handlesBySource[sourceIdentifier] else { return } 60 | 61 | source.removeObserver(handle: handle) 62 | } 63 | 64 | // MARK: MutableLiveData (Internal) 65 | 66 | internal func removeAllSources() { 67 | for handle in self.handlesBySource.values { 68 | self.removeObserver(handle: handle) 69 | } 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /Sources/ArchitectureComponents/LiveData/MutableLiveData.swift: -------------------------------------------------------------------------------- 1 | // ArchitectureComponents 2 | // Copyright (c) 2017 SPRI, LLC . Some rights reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | import Foundation 17 | 18 | 19 | /// LiveData subclass that exposes methods for updating the value. 20 | open class MutableLiveData: LiveData, ObserverListControllerDelegate { 21 | 22 | // MARK: Types 23 | 24 | private indirect enum ValueHolder { 25 | case empty 26 | case filled(Type) 27 | } 28 | 29 | // MARK: Properties 30 | 31 | public override var value: Type { 32 | willSet { 33 | assert(Thread.isMainThread) 34 | } 35 | didSet { 36 | self.controller.valueChanged(value: self.value) 37 | } 38 | } 39 | 40 | private var nextValue: ValueHolder 41 | private let semaphore: DispatchSemaphore 42 | 43 | private let controller: ObserverListController 44 | 45 | // MARK: Init / Deinit 46 | 47 | public override init(initialValue: Type) { 48 | self.controller = ObserverListController() 49 | self.nextValue = .empty 50 | self.semaphore = DispatchSemaphore(value: 1) 51 | 52 | super.init(initialValue: initialValue) 53 | 54 | self.controller.delegate = self 55 | } 56 | 57 | // MARK: LiveData 58 | 59 | public override var hasObservers: Bool { 60 | assert(Thread.isMainThread) 61 | return self.controller.hasObservers 62 | } 63 | 64 | public override var hasActiveObservers: Bool { 65 | assert(Thread.isMainThread) 66 | return self.controller.hasActiveObservers 67 | } 68 | 69 | public override func observe(owner: LifecycleOwner, observer: @escaping Observer) -> ObserverHandle { 70 | assert(Thread.isMainThread) 71 | let observerController = self.controller.insert(owner: owner, observer: observer) 72 | return observerController.handle 73 | } 74 | 75 | public override func observeForever(observer: @escaping Observer) -> ObserverHandle { 76 | assert(Thread.isMainThread) 77 | let observerController = self.controller.insert(observer: observer) 78 | return observerController.handle 79 | } 80 | 81 | public override func removeObserver(handle: ObserverHandle) { 82 | assert(Thread.isMainThread) 83 | self.controller.remove(handle: handle) 84 | } 85 | 86 | public override func removeObservers(owner: LifecycleOwner) { 87 | assert(Thread.isMainThread) 88 | self.controller.removeAllWithOwner(owner) 89 | } 90 | 91 | // MARK: ObserverListControllerDelegate 92 | 93 | internal final func observerListDidActivate(_ list: ObserverListController) { 94 | self.onActive() 95 | } 96 | 97 | internal final func observerListDidDeactivate(_ list: ObserverListController) { 98 | self.onInactive() 99 | } 100 | 101 | // MARK: MutableLiveData 102 | 103 | // TODO: write tests for this function 104 | open func onActive() { } 105 | 106 | // TODO: write tests for this function 107 | open func onInactive() { } 108 | 109 | public final func postValue(_ value: Type) { 110 | self.semaphore.wait() 111 | self.nextValue = .filled(value) 112 | self.semaphore.signal() 113 | 114 | DispatchQueue.main.async { 115 | self.semaphore.wait() 116 | let nextValue = self.nextValue 117 | self.semaphore.signal() 118 | 119 | // Using switch instead of `if case .filled(let latestValue)` to 120 | // handle the case when Type is optional and the value is nil. 121 | switch nextValue { 122 | case .empty: break 123 | case .filled(let latestValue): 124 | self.value = latestValue 125 | } 126 | } 127 | } 128 | 129 | } 130 | -------------------------------------------------------------------------------- /Sources/ArchitectureComponents/LiveData/Swift/LiveDataError.swift: -------------------------------------------------------------------------------- 1 | // ArchitectureComponents 2 | // Copyright (c) 2017 SPRI, LLC . Some rights reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | 17 | import Foundation 18 | 19 | 20 | public enum LiveDataError: Swift.Error { 21 | case sourceAlreadyAddedToMediator 22 | } 23 | -------------------------------------------------------------------------------- /Sources/ArchitectureComponents/LiveData/Swift/ObserverController.swift: -------------------------------------------------------------------------------- 1 | // ArchitectureComponents 2 | // Copyright (c) 2017 SPRI, LLC . Some rights reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | import Foundation 17 | 18 | 19 | internal class ObserverController: LifecycleObserver { 20 | 21 | // MARK: Types 22 | 23 | public typealias Observer = (Type) -> Void 24 | 25 | private indirect enum ObservedValue { 26 | case empty 27 | case filled(Type) 28 | } 29 | 30 | // MARK: Properties 31 | 32 | public let handle: ObserverHandle 33 | public let forever: Bool 34 | 35 | private let observer: Observer 36 | 37 | public weak var delegate: ObserverControllerDelegate? 38 | 39 | public weak var owner: LifecycleOwner? 40 | 41 | private var lastObservedValue: ObservedValue 42 | private var nextObservedValue: ObservedValue 43 | 44 | public var active: Bool { 45 | // Forever observers are always active 46 | guard !self.forever else { return true } 47 | // If the owner has been released, then this observer is not active 48 | guard let owner = self.owner else { return false } 49 | 50 | let active: Bool 51 | switch owner.lifecycle.currentState { 52 | case .started, .resumed: active = true 53 | default: active = false 54 | } 55 | return active 56 | } 57 | 58 | // MARK: Init / Deinit 59 | 60 | public init(owner: LifecycleOwner, observer: @escaping Observer) { 61 | let observerObject = observer as AnyObject 62 | let handleValue = ObjectIdentifier(observerObject) 63 | 64 | self.handle = ObserverHandle(value: handleValue) 65 | self.owner = owner 66 | self.observer = observer 67 | self.forever = false 68 | 69 | self.lastObservedValue = .empty 70 | self.nextObservedValue = .empty 71 | 72 | owner.lifecycle.addObserver(self) 73 | } 74 | 75 | public init(observer: @escaping Observer) { 76 | let observerObject = observer as AnyObject 77 | let handleValue = ObjectIdentifier(observerObject) 78 | 79 | self.handle = ObserverHandle(value: handleValue) 80 | self.owner = nil 81 | self.observer = observer 82 | self.forever = true 83 | 84 | self.lastObservedValue = .empty 85 | self.nextObservedValue = .empty 86 | } 87 | 88 | deinit { 89 | self.owner?.lifecycle.removeObserver(self) 90 | } 91 | 92 | // MARK: LifecycleObserver 93 | 94 | public func onCreate(owner: LifecycleOwner) { } 95 | 96 | public func onStart(owner: LifecycleOwner) { 97 | self.delegate?.observerDidActivate(self) 98 | 99 | // Written this way instead of with "if case .filled(...)" to 100 | // handle nil values properly. 101 | switch self.nextObservedValue { 102 | case .empty: break 103 | case .filled(let nextValue): 104 | notifyObserverIfChanged(value: nextValue) 105 | } 106 | } 107 | 108 | public func onResume(owner: LifecycleOwner) { } 109 | 110 | public func onPause(owner: LifecycleOwner) { } 111 | 112 | public func onStop(owner: LifecycleOwner) { 113 | self.delegate?.observerDidDeactivate(self) 114 | } 115 | 116 | public func onDestroy(owner: LifecycleOwner) { 117 | owner.lifecycle.removeObserver(self) 118 | self.owner = nil 119 | } 120 | 121 | // MARK: ObserverController 122 | 123 | public func hasOwner(_ owner: LifecycleOwner) -> Bool { 124 | guard let myOwner = self.owner else { return false } 125 | let sameOwner = (myOwner === owner) 126 | return sameOwner 127 | } 128 | 129 | public func valueChanged(value: Type) { 130 | if self.active { 131 | self.notifyObserverIfChanged(value: value) 132 | } else { 133 | self.nextObservedValue = .filled(value) 134 | } 135 | } 136 | 137 | // MARK: Private 138 | 139 | private func notifyObserverIfChanged(value nextValue: Type) { 140 | switch self.lastObservedValue { 141 | case .empty: 142 | self.notifyObserver(value: nextValue) 143 | case .filled(let lastValue): 144 | let changed = !self.equal(last: lastValue, next: nextValue) 145 | if changed { 146 | self.notifyObserver(value: nextValue) 147 | } 148 | } 149 | } 150 | 151 | private func notifyObserver(value: Type) { 152 | self.lastObservedValue = .filled(value) 153 | self.nextObservedValue = .empty 154 | self.observer(value) 155 | } 156 | 157 | // Private (Comparison) 158 | 159 | private func equal(last: EquatableType?, next: EquatableType?) -> Bool where EquatableType: Equatable { 160 | let result: Bool 161 | 162 | if let last = last, let next = next { 163 | result = (last == next) 164 | } else if last == nil && next == nil { 165 | result = true 166 | } else { 167 | result = false 168 | } 169 | 170 | return result 171 | } 172 | 173 | private func equal(last: Type, next: Type) -> Bool { 174 | let lastObject = last as AnyObject? 175 | let nextObject = next as AnyObject? 176 | 177 | let result = (lastObject === nextObject) 178 | return result 179 | } 180 | 181 | } 182 | 183 | internal protocol ObserverControllerDelegate: class { 184 | func observerDidActivate(_ observer: ObserverController) 185 | func observerDidDeactivate(_ observer: ObserverController) 186 | } 187 | -------------------------------------------------------------------------------- /Sources/ArchitectureComponents/LiveData/Swift/ObserverHandle.swift: -------------------------------------------------------------------------------- 1 | // ArchitectureComponents 2 | // Copyright (c) 2017 SPRI, LLC . Some rights reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | import Foundation 17 | 18 | 19 | public struct ObserverHandle: Equatable, Hashable { 20 | 21 | private let value: ObjectIdentifier 22 | 23 | internal init(value: ObjectIdentifier) { 24 | self.value = value 25 | } 26 | 27 | // MARK: Equatable 28 | 29 | public static func ==(lhs: ObserverHandle, rhs: ObserverHandle) -> Bool { 30 | let equal = (lhs.value == rhs.value) 31 | return equal 32 | } 33 | 34 | // MARK: Hashable 35 | 36 | public var hashValue: Int { 37 | let result = self.value.hashValue 38 | return result 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /Sources/ArchitectureComponents/LiveData/Swift/ObserverListController.swift: -------------------------------------------------------------------------------- 1 | // ArchitectureComponents 2 | // Copyright (c) 2017 SPRI, LLC . Some rights reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | import Foundation 17 | 18 | 19 | internal class ObserverListController: ObserverControllerDelegate { 20 | 21 | // MARK: Type 22 | 23 | public typealias Observer = (Type) -> Void 24 | 25 | // MARK: Properties 26 | 27 | private var observerControllers: [ObserverController] = [] 28 | private var active: Bool = false { 29 | didSet { 30 | if oldValue != active { 31 | if active { 32 | self.delegate?.observerListDidActivate(self) 33 | } else { 34 | self.delegate?.observerListDidDeactivate(self) 35 | } 36 | } 37 | } 38 | } 39 | 40 | public weak var delegate: ObserverListControllerDelegate? 41 | 42 | public var hasActiveObservers: Bool { 43 | let found = self.observerControllers.contains(where: { $0.active }) 44 | return found 45 | } 46 | 47 | public var hasObservers: Bool { 48 | let empty = self.observerControllers.isEmpty 49 | return !empty 50 | } 51 | 52 | // MARK: ObserverControllerDelegate 53 | 54 | public func observerDidActivate(_ observer: ObserverController) { 55 | self.active = self.hasActiveObservers 56 | } 57 | 58 | public func observerDidDeactivate(_ observer: ObserverController) { 59 | self.active = self.hasActiveObservers 60 | } 61 | 62 | // MARK: ObserverListController (Adding and Removing Objects) 63 | 64 | public func insert(owner: LifecycleOwner, observer: @escaping Observer) -> ObserverController { 65 | tidyObserverControllers() 66 | 67 | let controller = ObserverController(owner: owner, observer: observer) 68 | controller.delegate = self 69 | self.observerControllers.append(controller) 70 | 71 | self.active = self.hasActiveObservers 72 | 73 | return controller 74 | } 75 | 76 | public func insert(observer: @escaping Observer) -> ObserverController { 77 | tidyObserverControllers() 78 | 79 | let controller = ObserverController(observer: observer) 80 | self.observerControllers.append(controller) 81 | 82 | self.active = self.hasActiveObservers 83 | 84 | return controller 85 | } 86 | 87 | public func remove(handle: ObserverHandle) { 88 | tidyObserverControllers() 89 | 90 | let remainingObservers = self.observerControllers.filter({ $0.handle != handle }) 91 | self.observerControllers = remainingObservers 92 | 93 | self.active = self.hasActiveObservers 94 | } 95 | 96 | public func removeAllWithOwner(_ owner: LifecycleOwner) { 97 | tidyObserverControllers() 98 | 99 | let remainingObservers = self.observerControllers.filter({ !$0.hasOwner(owner) }) 100 | self.observerControllers = remainingObservers 101 | 102 | self.active = self.hasActiveObservers 103 | } 104 | 105 | // MARK: ObserverListController (Notifying Observers) 106 | 107 | public func valueChanged(value: Type) { 108 | self.observerControllers.forEach { $0.valueChanged(value: value) } 109 | } 110 | 111 | // MARK: Private 112 | 113 | private func tidyObserverControllers() { 114 | // remove observers that have had their lifecycle owners released 115 | let remainingObservers = self.observerControllers.filter({ $0.forever || $0.owner != nil }) 116 | self.observerControllers = remainingObservers 117 | } 118 | 119 | } 120 | 121 | 122 | internal protocol ObserverListControllerDelegate: class { 123 | func observerListDidActivate(_ list: ObserverListController) 124 | func observerListDidDeactivate(_ list: ObserverListController) 125 | } 126 | -------------------------------------------------------------------------------- /Sources/ArchitectureComponents/LiveData/Swift/SwitchedLiveData.swift: -------------------------------------------------------------------------------- 1 | // ArchitectureComponents 2 | // Copyright (c) 2017 SPRI, LLC . Some rights reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | import Foundation 17 | 18 | 19 | internal class SwitchedLiveData: LiveData { 20 | 21 | // MARK: Properties 22 | 23 | private var sourceHandle: ObserverHandle? 24 | private var source: LiveData 25 | private let transform: (InType) -> LiveData 26 | private let trigger: LiveData 27 | private var triggerHandle: ObserverHandle? 28 | 29 | // MARK: Init / Deinit 30 | 31 | internal init(trigger: LiveData, transform: @escaping (InType) -> LiveData) { 32 | let initialInValue = trigger.value 33 | let initialOutLiveData = transform(initialInValue) 34 | let initialOutValue = initialOutLiveData.value 35 | 36 | self.sourceHandle = nil 37 | self.source = initialOutLiveData 38 | self.transform = transform 39 | self.trigger = trigger 40 | self.triggerHandle = nil 41 | 42 | super.init(initialValue: initialOutValue) 43 | 44 | // Written using a block instead of a method reference so that 45 | // reference to `self` is weak. 46 | self.triggerHandle = self.trigger.observeForever { [weak self] (inValue) in 47 | self?.createAndObserverNextLiveData(inValue: inValue) 48 | } 49 | 50 | // Start observing self.source 51 | self.createAndObserverNextLiveData(inValue: initialInValue) 52 | } 53 | 54 | deinit { 55 | if let sourceHandle = self.sourceHandle { 56 | self.source.removeObserver(handle: sourceHandle) 57 | } 58 | if let triggerHandle = self.triggerHandle { 59 | self.trigger.removeObserver(handle: triggerHandle) 60 | } 61 | } 62 | 63 | // MARK: Private 64 | 65 | private func createAndObserverNextLiveData(inValue: InType) { 66 | // Create the next LiveData. 67 | // 68 | // To avoid re-creating the first LiveData that is generated 69 | // by the initializer, this section checks if sourceHandle is set 70 | // before creating a new LiveData. This may be a Premature 71 | // Optimization. 72 | if let handle = self.sourceHandle { 73 | // Stop observing the current LiveData 74 | self.source.removeObserver(handle: handle) 75 | 76 | // Create the next LiveData, overwriting the current one 77 | self.source = self.transform(inValue) 78 | } 79 | 80 | // Update this LiveData's value whenever the new LiveData 81 | // updates itself 82 | self.sourceHandle = self.source.observeForever { [weak self] (outValue) in 83 | self?.value = outValue 84 | } 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /Sources/ArchitectureComponents/LiveData/Swift/TransformedLiveData.swift: -------------------------------------------------------------------------------- 1 | // ArchitectureComponents 2 | // Copyright (c) 2017 SPRI, LLC . Some rights reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | import Foundation 17 | 18 | 19 | internal class TransformedLiveData: LiveData { 20 | 21 | // MARK: Properties 22 | 23 | private let source: LiveData 24 | private var sourceHandle: ObserverHandle? 25 | private let transform: (InType) -> OutType 26 | 27 | // MARK: Init / Deinit 28 | 29 | internal init(source: LiveData, transform: @escaping (InType) -> OutType) { 30 | self.source = source 31 | self.sourceHandle = nil 32 | self.transform = transform 33 | 34 | let initialValueIn = source.value 35 | let initialValueOut = transform(initialValueIn) 36 | super.init(initialValue: initialValueOut) 37 | 38 | self.sourceHandle = source.observeForever { [weak self] (inValue) in 39 | guard let strongSelf = self else { return } 40 | 41 | let outValue = strongSelf.transform(inValue) 42 | strongSelf.value = outValue 43 | } 44 | } 45 | 46 | deinit { 47 | if let sourceHandle = self.sourceHandle { 48 | self.source.removeObserver(handle: sourceHandle) 49 | } 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /Sources/ArchitectureComponents/LiveData/Transformations.swift: -------------------------------------------------------------------------------- 1 | // ArchitectureComponents 2 | // Copyright (c) 2017 SPRI, LLC . Some rights reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | import Foundation 17 | 18 | 19 | public class Transformations { 20 | 21 | // TODO: write tests for this function 22 | public static func map(source: LiveData, transform: @escaping (X) -> Y) -> LiveData { 23 | let transformedLiveData = TransformedLiveData(source: source, transform: transform) 24 | return transformedLiveData 25 | } 26 | 27 | // TODO: write tests for this function 28 | public static func switchMap(trigger: LiveData, transform: @escaping (X) -> LiveData) -> LiveData { 29 | let switchedLiveData = SwitchedLiveData(trigger: trigger, transform: transform) 30 | return switchedLiveData 31 | } 32 | 33 | /// Cannot be instantiated. 34 | private init() { } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /Sources/ArchitectureComponents/UIKit/LifecycleTableViewController.swift: -------------------------------------------------------------------------------- 1 | // ArchitectureComponents 2 | // Copyright (c) 2017 SPRI, LLC . Some rights reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | #if os(iOS) 17 | 18 | import UIKit 19 | 20 | 21 | open class LifecycleTableViewController: UITableViewController, LifecycleOwner { 22 | // TODO: write tests for this class 23 | 24 | // MARK: Properties 25 | 26 | private var lifecycleAppState: UIApplicationState = .inactive 27 | private var lifecycleViewDisplayed: Bool = false 28 | 29 | // This property is defined as lazy to work-around Swift initialization 30 | // requirements. You cannot use `self` before calling `super`, so 31 | // `lifecycleRegistry` cannot be initialized before `super`. However, since 32 | // `lifecycleRegistry` is not optional, it must be assigned before calling 33 | // super. The trick is to define the `lifecycleRegistry` property as lazy, 34 | // allowing us to avoid making it optional AND letting us use `self` in its 35 | // initializer. 36 | private lazy var lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(provider: self) 37 | 38 | // MARK: init / deinit 39 | 40 | public override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { 41 | super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) 42 | self.sharedInitialization() 43 | } 44 | 45 | public required init?(coder aDecoder: NSCoder) { 46 | super.init(coder: aDecoder) 47 | self.sharedInitialization() 48 | } 49 | 50 | deinit { 51 | NotificationCenter.default.removeObserver(self) 52 | 53 | self.onDestroy() 54 | self.lifecycleRegistry.markState(.destroyed) 55 | } 56 | 57 | // MARK: UIViewController 58 | 59 | open override func viewWillAppear(_ animated: Bool) { 60 | super.viewWillAppear(animated) 61 | 62 | self.lifecycleViewDisplayed = true 63 | self.updateLifecycleState() 64 | } 65 | 66 | open override func viewDidDisappear(_ animated: Bool) { 67 | super.viewDidDisappear(animated) 68 | 69 | self.lifecycleViewDisplayed = false 70 | self.updateLifecycleState() 71 | } 72 | 73 | // MARK: LifecycleOwner 74 | 75 | public var lifecycle: Lifecycle { return self.lifecycleRegistry } 76 | 77 | // MARK: LifecycleViewController 78 | 79 | open func onCreate() { } 80 | 81 | open func onStart() { } 82 | 83 | open func onResume() { } 84 | 85 | open func onPause() { } 86 | 87 | open func onStop() { } 88 | 89 | open func onDestroy() { } 90 | 91 | // MARK: Private (Initialization) 92 | 93 | private func sharedInitialization() { 94 | self.lifecycleAppState = UIApplication.shared.applicationState 95 | self.lifecycleViewDisplayed = false 96 | 97 | self.registerForNotifications() 98 | 99 | self.updateLifecycleState() 100 | } 101 | 102 | // MARK: Private (Notifications) 103 | 104 | @objc private func handleApplicationStateChangeNotification(_ note: Notification) { 105 | self.lifecycleAppState = UIApplication.shared.applicationState 106 | self.updateLifecycleState() 107 | } 108 | 109 | private func registerForNotifications() { 110 | let center = NotificationCenter.default 111 | let app = UIApplication.shared 112 | 113 | center.addObserver( 114 | self, 115 | selector: #selector(LifecycleTableViewController.handleApplicationStateChangeNotification(_:)), 116 | name: .UIApplicationDidBecomeActive, 117 | object: app 118 | ) 119 | center.addObserver( 120 | self, 121 | selector: #selector(LifecycleTableViewController.handleApplicationStateChangeNotification(_:)), 122 | name: .UIApplicationDidEnterBackground, 123 | object: app 124 | ) 125 | center.addObserver( 126 | self, 127 | selector: #selector(LifecycleTableViewController.handleApplicationStateChangeNotification(_:)), 128 | name: .UIApplicationWillEnterForeground, 129 | object: app 130 | ) 131 | center.addObserver( 132 | self, 133 | selector: #selector(LifecycleTableViewController.handleApplicationStateChangeNotification(_:)), 134 | name: .UIApplicationWillResignActive, 135 | object: app 136 | ) 137 | } 138 | 139 | // MARK: Private (Lifecycle State Change) 140 | 141 | internal static func lifecycleStateForVCState(appState: UIApplicationState, viewDisplayed: Bool) -> LifecycleState { 142 | // DEVELOPER NOTE: This method has `internal` access to allow for 143 | // automated testing. 144 | 145 | let lcState: LifecycleState 146 | 147 | if viewDisplayed { 148 | switch appState { 149 | case .active: lcState = .resumed 150 | case .inactive: lcState = .started 151 | case .background: lcState = .created 152 | } 153 | } else { 154 | lcState = .created 155 | } 156 | 157 | return lcState 158 | } 159 | 160 | private func updateLifecycleState() { 161 | // determine the state we should be in 162 | let targetState = LifecycleViewController.lifecycleStateForVCState( 163 | appState: self.lifecycleAppState, 164 | viewDisplayed: self.lifecycleViewDisplayed 165 | ) 166 | 167 | // transition to the target state, ensuring our onCreate(), onStart(), 168 | // etc. are called before the observers are notified of the state 169 | // changes 170 | self.lifecycleRegistry.markState(targetState) { (event) in 171 | switch event { 172 | case .create: self.onCreate() 173 | case .start: self.onStart() 174 | case .resume: self.onResume() 175 | case .pause: self.onPause() 176 | case .stop: self.onStop() 177 | case .destroy: self.onDestroy() 178 | } 179 | } 180 | } 181 | 182 | } 183 | 184 | #endif 185 | -------------------------------------------------------------------------------- /Sources/ArchitectureComponents/UIKit/LifecycleViewController.swift: -------------------------------------------------------------------------------- 1 | // ArchitectureComponents 2 | // Copyright (c) 2017 SPRI, LLC . Some rights reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | #if os(iOS) 17 | 18 | import UIKit 19 | 20 | 21 | open class LifecycleViewController: UIViewController, LifecycleOwner { 22 | // TODO: write tests for this class 23 | 24 | // MARK: Properties 25 | 26 | private var lifecycleAppState: UIApplicationState = .inactive 27 | private var lifecycleViewDisplayed: Bool = false 28 | 29 | // This property is defined as lazy to work-around Swift initialization 30 | // requirements. You cannot use `self` before calling `super`, so 31 | // `lifecycleRegistry` cannot be initialized before `super`. However, since 32 | // `lifecycleRegistry` is not optional, it must be assigned before calling 33 | // super. The trick is to define the `lifecycleRegistry` property as lazy, 34 | // allowing us to avoid making it optional AND letting us use `self` in its 35 | // initializer. 36 | private lazy var lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(provider: self) 37 | 38 | // MARK: init / deinit 39 | 40 | public override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { 41 | super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) 42 | self.sharedInitialization() 43 | } 44 | 45 | public required init?(coder aDecoder: NSCoder) { 46 | super.init(coder: aDecoder) 47 | self.sharedInitialization() 48 | } 49 | 50 | deinit { 51 | NotificationCenter.default.removeObserver(self) 52 | 53 | self.onDestroy() 54 | self.lifecycleRegistry.markState(.destroyed) 55 | } 56 | 57 | // MARK: UIViewController 58 | 59 | open override func viewWillAppear(_ animated: Bool) { 60 | super.viewWillAppear(animated) 61 | 62 | self.lifecycleViewDisplayed = true 63 | self.updateLifecycleState() 64 | } 65 | 66 | open override func viewDidDisappear(_ animated: Bool) { 67 | super.viewDidDisappear(animated) 68 | 69 | self.lifecycleViewDisplayed = false 70 | self.updateLifecycleState() 71 | } 72 | 73 | // MARK: LifecycleOwner 74 | 75 | public var lifecycle: Lifecycle { return self.lifecycleRegistry } 76 | 77 | // MARK: LifecycleViewController 78 | 79 | open func onCreate() { } 80 | 81 | open func onStart() { } 82 | 83 | open func onResume() { } 84 | 85 | open func onPause() { } 86 | 87 | open func onStop() { } 88 | 89 | open func onDestroy() { } 90 | 91 | // MARK: Private (Initialization) 92 | 93 | private func sharedInitialization() { 94 | self.lifecycleAppState = UIApplication.shared.applicationState 95 | self.lifecycleViewDisplayed = false 96 | 97 | self.registerForNotifications() 98 | 99 | self.updateLifecycleState() 100 | } 101 | 102 | // MARK: Private (Notifications) 103 | 104 | @objc private func handleApplicationStateChangeNotification(_ note: Notification) { 105 | self.lifecycleAppState = UIApplication.shared.applicationState 106 | self.updateLifecycleState() 107 | } 108 | 109 | private func registerForNotifications() { 110 | let center = NotificationCenter.default 111 | let app = UIApplication.shared 112 | 113 | center.addObserver( 114 | self, 115 | selector: #selector(LifecycleViewController.handleApplicationStateChangeNotification(_:)), 116 | name: .UIApplicationDidBecomeActive, 117 | object: app 118 | ) 119 | center.addObserver( 120 | self, 121 | selector: #selector(LifecycleViewController.handleApplicationStateChangeNotification(_:)), 122 | name: .UIApplicationDidEnterBackground, 123 | object: app 124 | ) 125 | center.addObserver( 126 | self, 127 | selector: #selector(LifecycleViewController.handleApplicationStateChangeNotification(_:)), 128 | name: .UIApplicationWillEnterForeground, 129 | object: app 130 | ) 131 | center.addObserver( 132 | self, 133 | selector: #selector(LifecycleViewController.handleApplicationStateChangeNotification(_:)), 134 | name: .UIApplicationWillResignActive, 135 | object: app 136 | ) 137 | } 138 | 139 | // MARK: Private (Lifecycle State Change) 140 | 141 | internal static func lifecycleStateForVCState(appState: UIApplicationState, viewDisplayed: Bool) -> LifecycleState { 142 | // DEVELOPER NOTE: This method has `internal` access to allow for 143 | // automated testing. 144 | 145 | let lcState: LifecycleState 146 | 147 | if viewDisplayed { 148 | switch appState { 149 | case .active: lcState = .resumed 150 | case .inactive: lcState = .started 151 | case .background: lcState = .created 152 | } 153 | } else { 154 | lcState = .created 155 | } 156 | 157 | return lcState 158 | } 159 | 160 | private func updateLifecycleState() { 161 | // determine the state we should be in 162 | let targetState = LifecycleViewController.lifecycleStateForVCState( 163 | appState: self.lifecycleAppState, 164 | viewDisplayed: self.lifecycleViewDisplayed 165 | ) 166 | 167 | // transition to the target state, ensuring our onCreate(), onStart(), 168 | // etc. are called before the observers are notified of the state 169 | // changes 170 | self.lifecycleRegistry.markState(targetState) { (event) in 171 | switch event { 172 | case .create: self.onCreate() 173 | case .start: self.onStart() 174 | case .resume: self.onResume() 175 | case .pause: self.onPause() 176 | case .stop: self.onStop() 177 | case .destroy: self.onDestroy() 178 | } 179 | } 180 | } 181 | 182 | } 183 | 184 | #endif 185 | -------------------------------------------------------------------------------- /Sources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 0.1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSPrincipalClass 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Tests/ArchitectureComponentsTests/Lifecycle/LifecycleStateTests.swift: -------------------------------------------------------------------------------- 1 | // ArchitectureComponents 2 | // Copyright (c) 2017 SPRI, LLC . Some rights reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | import XCTest 17 | @testable import ArchitectureComponents 18 | 19 | class LifecycleStateTests: XCTestCase { 20 | 21 | // MARK: - eventsToState 22 | 23 | // MARK: (Single-Step) 24 | 25 | func testEventsToState_initializeToCreated_returnsCreate() { 26 | let initialized = LifecycleState.initialized 27 | do { 28 | let events = try initialized.eventsToState(.created) 29 | XCTAssertEqual([.create], events) 30 | } catch { XCTFail() } 31 | } 32 | 33 | func testEventsToState_createdToStarted_returnsStart() { 34 | let created = LifecycleState.created 35 | do { 36 | let events = try created.eventsToState(.started) 37 | XCTAssertEqual([.start], events) 38 | } catch { XCTFail() } 39 | } 40 | 41 | func testEventsToState_startedToResumed_returnsResume() { 42 | let started = LifecycleState.started 43 | do { 44 | let events = try started.eventsToState(.resumed) 45 | XCTAssertEqual([.resume], events) 46 | } catch { XCTFail() } 47 | } 48 | 49 | func testEventsToState_resumedToStarted_returnsPause() { 50 | let resumed = LifecycleState.resumed 51 | do { 52 | let events = try resumed.eventsToState(.started) 53 | XCTAssertEqual([.pause], events) 54 | } catch { XCTFail() } 55 | } 56 | 57 | func testEventsToState_startedToCreated_returnsStop() { 58 | let started = LifecycleState.started 59 | do { 60 | let events = try started.eventsToState(.created) 61 | XCTAssertEqual([.stop], events) 62 | } catch { XCTFail() } 63 | } 64 | 65 | func testEventsToState_createdToDestroyed_returnsDestroy() { 66 | let created = LifecycleState.created 67 | do { 68 | let events = try created.eventsToState(.destroyed) 69 | XCTAssertEqual([.destroy], events) 70 | } catch { XCTFail() } 71 | } 72 | 73 | // MARK: (Multi-Step) 74 | 75 | func testEventsToState_initializedToStarted_returnsCreateAndStart() { 76 | let initialized = LifecycleState.initialized 77 | do { 78 | let events = try initialized.eventsToState(.started) 79 | XCTAssertEqual([.create, .start], events) 80 | } catch { XCTFail() } 81 | } 82 | 83 | func testEventsToState_initializedToResumed_returnsCreateAndStartAndResume() { 84 | let initialized = LifecycleState.initialized 85 | do { 86 | let events = try initialized.eventsToState(.resumed) 87 | XCTAssertEqual([.create, .start, .resume], events) 88 | } catch { XCTFail() } 89 | } 90 | 91 | func testEventsToState_initializedToDestroyed_returnsCreateAndDestroy() { 92 | let initialized = LifecycleState.initialized 93 | do { 94 | let events = try initialized.eventsToState(.destroyed) 95 | XCTAssertEqual([.create, .destroy], events) 96 | } catch { XCTFail() } 97 | } 98 | 99 | func testEventsToState_createdToResumed_returnsStartAndResume() { 100 | let created = LifecycleState.created 101 | do { 102 | let events = try created.eventsToState(.resumed) 103 | XCTAssertEqual([.start, .resume], events) 104 | } catch { XCTFail() } 105 | } 106 | 107 | func testEventsToState_startedToDestroyed_returnsStopAndDestroy() { 108 | let started = LifecycleState.started 109 | do { 110 | let events = try started.eventsToState(.destroyed) 111 | XCTAssertEqual([.stop, .destroy], events) 112 | } catch { XCTFail() } 113 | } 114 | 115 | func testEventsToState_resumedToCreated_returnsPauseAndStop() { 116 | let resumed = LifecycleState.resumed 117 | do { 118 | let events = try resumed.eventsToState(.created) 119 | XCTAssertEqual([.pause, .stop], events) 120 | } catch { XCTFail() } 121 | } 122 | 123 | func testEventsToState_resumedToDestroyed_returnsPauseAndStopAndDestroy() { 124 | let resumed = LifecycleState.resumed 125 | do { 126 | let events = try resumed.eventsToState(.destroyed) 127 | XCTAssertEqual([.pause, .stop, .destroy], events) 128 | } catch { XCTFail() } 129 | } 130 | 131 | // MARK: (Same State) 132 | 133 | func testEventsToState_fromAndToAreSame_returnsEmptyArray() { 134 | let allStates: [LifecycleState] = [.initialized, .destroyed, .created, .started, .resumed] 135 | for state in allStates { 136 | do { 137 | let events = try state.eventsToState(state) 138 | XCTAssertEqual(0, events.count) 139 | } catch { XCTFail() } 140 | } 141 | } 142 | 143 | // MARK: (Invalid) 144 | 145 | func testEventsToState_destroyedToCreated_throws() { 146 | let destroyed = LifecycleState.destroyed 147 | do { 148 | _ = try destroyed.eventsToState(.created) 149 | XCTFail() 150 | } catch (let error) { 151 | if case LifecycleError.invalidStateTransition(from: let f, to: let t) = error { 152 | XCTAssertEqual(.destroyed, f) 153 | XCTAssertEqual(.created, t) 154 | } else { 155 | XCTFail() 156 | } 157 | } 158 | } 159 | 160 | func testEventsToState_createdToInitialized_throws() { 161 | let created = LifecycleState.created 162 | do { 163 | _ = try created.eventsToState(.initialized) 164 | XCTFail() 165 | } catch (let error) { 166 | if case LifecycleError.invalidStateTransition(from: let f, to: let t) = error { 167 | XCTAssertEqual(.created, f) 168 | XCTAssertEqual(.initialized, t) 169 | } else { 170 | XCTFail() 171 | } 172 | } 173 | } 174 | 175 | } 176 | -------------------------------------------------------------------------------- /Tests/ArchitectureComponentsTests/LiveData/Helpers/MockLifecycleOwner.swift: -------------------------------------------------------------------------------- 1 | // ArchitectureComponents 2 | // Copyright (c) 2017 SPRI, LLC . Some rights reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | import ArchitectureComponents 17 | 18 | 19 | class MockLifecycleOwner: LifecycleOwner { 20 | 21 | // MARK: Properties 22 | 23 | private let initialState: LifecycleState 24 | 25 | private lazy var registry: LifecycleRegistry = LifecycleRegistry( 26 | provider: self, 27 | initialState: self.initialState 28 | ) 29 | 30 | // MARK: Init / Deinit 31 | 32 | init(initialState: LifecycleState) { 33 | self.initialState = initialState 34 | } 35 | 36 | // MARK: LifecycleOwner 37 | 38 | public var lifecycle: Lifecycle { return self.registry } 39 | 40 | // MARK: MockLifecycleOwner 41 | 42 | func transitionToState(_ state: LifecycleState) { 43 | self.registry.markState(state) 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /Tests/ArchitectureComponentsTests/LiveData/Helpers/MockLiveDataObserver.swift: -------------------------------------------------------------------------------- 1 | // ArchitectureComponents 2 | // Copyright (c) 2017 SPRI, LLC . Some rights reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | import ArchitectureComponents 17 | 18 | 19 | class MockLiveDataObserver { 20 | 21 | /// Used by tests to check the sequence of values sent to this class. 22 | var valuesObserved: [Type] = [] 23 | 24 | /// Used to register this class as an observer on a LiveData object. 25 | /// 26 | /// Example: 27 | /// 28 | /// let liveData = LiveData 29 | /// let mockObserver = MockLiveDataObserver 30 | /// liveData.observe(mockObserver.observer) 31 | var observer: LiveData.Observer { 32 | return self.onChange(value:) 33 | } 34 | 35 | /// Internal implementation of LiveData.Observer type. 36 | private func onChange(value: Type) { 37 | self.valuesObserved.append(value) 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /Tests/ArchitectureComponentsTests/LiveData/LiveData_NotificationTests.swift: -------------------------------------------------------------------------------- 1 | // ArchitectureComponents 2 | // Copyright (c) 2017 SPRI, LLC . Some rights reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | import XCTest 17 | @testable import ArchitectureComponents 18 | 19 | 20 | class LiveData_NotificationTests: XCTestCase { 21 | 22 | func testForeverObserverNotifiedOnEveryChange() { 23 | // Setup 24 | let liveData = MutableLiveData(initialValue: -1) 25 | let mockObserver = MockLiveDataObserver() 26 | let handle = liveData.observeForever(observer: mockObserver.observer) 27 | defer { liveData.removeObserver(handle: handle) } 28 | 29 | // Actions 30 | liveData.value = 0 31 | liveData.value = nil 32 | liveData.value = 2 33 | liveData.value = 3 34 | liveData.value = 4 35 | 36 | // Assertions 37 | 38 | guard 5 == mockObserver.valuesObserved.count else { 39 | XCTFail("Expected to be notified 5 times of value changes: \(mockObserver.valuesObserved.count)") 40 | return 41 | } 42 | 43 | XCTAssertEqual(0, mockObserver.valuesObserved[0]) 44 | XCTAssertNil(mockObserver.valuesObserved[1]) 45 | XCTAssertEqual(2, mockObserver.valuesObserved[2]) 46 | XCTAssertEqual(3, mockObserver.valuesObserved[3]) 47 | XCTAssertEqual(4, mockObserver.valuesObserved[4]) 48 | } 49 | 50 | func testObserverNotNotifiedOfAnyChangeWhenLifecycleInitialized() { 51 | // Setup 52 | let liveData = MutableLiveData(initialValue: 0) 53 | let mockOwner = MockLifecycleOwner(initialState: .initialized) 54 | let mockObserver = MockLiveDataObserver() 55 | let handle = liveData.observe(owner: mockOwner, observer: mockObserver.observer) 56 | defer { liveData.removeObserver(handle: handle) } 57 | 58 | // Actions 59 | liveData.value = 1 60 | liveData.value = 1 61 | liveData.value = nil 62 | liveData.value = 2 63 | liveData.value = 3 64 | liveData.value = 5 65 | 66 | // Assertions 67 | XCTAssertEqual(0, mockObserver.valuesObserved.count) 68 | } 69 | 70 | func testObserverNotNotifiedOfAnyChangeWhenLifecycleCreated() { 71 | // Setup 72 | let liveData = MutableLiveData(initialValue: 0) 73 | let mockOwner = MockLifecycleOwner(initialState: .created) 74 | let mockObserver = MockLiveDataObserver() 75 | let handle = liveData.observe(owner: mockOwner, observer: mockObserver.observer) 76 | defer { liveData.removeObserver(handle: handle) } 77 | 78 | // Actions 79 | liveData.value = 1 80 | liveData.value = 1 81 | liveData.value = nil 82 | liveData.value = 2 83 | liveData.value = 3 84 | liveData.value = 5 85 | 86 | // Assertions 87 | XCTAssertEqual(0, mockObserver.valuesObserved.count) 88 | } 89 | 90 | func testObserverNotifiedOnEveryChangeWhenLifecycleStarted() { 91 | // Setup 92 | let liveData = MutableLiveData(initialValue: 0) 93 | let mockOwner = MockLifecycleOwner(initialState: .started) 94 | let mockObserver = MockLiveDataObserver() 95 | let handle = liveData.observe(owner: mockOwner, observer: mockObserver.observer) 96 | defer { liveData.removeObserver(handle: handle) } 97 | 98 | // Actions 99 | liveData.value = 1 100 | liveData.value = nil 101 | liveData.value = 2 102 | liveData.value = 3 103 | liveData.value = 5 104 | 105 | // Assertions 106 | 107 | guard 5 == mockObserver.valuesObserved.count else { 108 | XCTFail("Expected to be notified 5 times of value changes: \(mockObserver.valuesObserved.count)") 109 | return 110 | } 111 | 112 | XCTAssertEqual(1, mockObserver.valuesObserved[0]) 113 | XCTAssertNil(mockObserver.valuesObserved[1]) 114 | XCTAssertEqual(2, mockObserver.valuesObserved[2]) 115 | XCTAssertEqual(3, mockObserver.valuesObserved[3]) 116 | XCTAssertEqual(5, mockObserver.valuesObserved[4]) 117 | } 118 | 119 | func testObserverNotifiedOnEveryChangeWhenLifecycleResumed() { 120 | // Setup 121 | let liveData = MutableLiveData(initialValue: 0) 122 | let mockOwner = MockLifecycleOwner(initialState: .resumed) 123 | let mockObserver = MockLiveDataObserver() 124 | let handle = liveData.observe(owner: mockOwner, observer: mockObserver.observer) 125 | defer { liveData.removeObserver(handle: handle) } 126 | 127 | // Actions 128 | liveData.value = 1 129 | liveData.value = nil 130 | liveData.value = 2 131 | liveData.value = 3 132 | liveData.value = 5 133 | 134 | // Assertions 135 | 136 | guard 5 == mockObserver.valuesObserved.count else { 137 | XCTFail("Expected to be notified 5 times of value changes: \(mockObserver.valuesObserved.count)") 138 | return 139 | } 140 | 141 | XCTAssertEqual(1, mockObserver.valuesObserved[0]) 142 | XCTAssertNil(mockObserver.valuesObserved[1]) 143 | XCTAssertEqual(2, mockObserver.valuesObserved[2]) 144 | XCTAssertEqual(3, mockObserver.valuesObserved[3]) 145 | XCTAssertEqual(5, mockObserver.valuesObserved[4]) 146 | } 147 | 148 | func testObserverNotNotifiedOfAnyChangeWhenLifecycleDestroyed() { 149 | // Setup 150 | let liveData = MutableLiveData(initialValue: 0) 151 | let mockOwner = MockLifecycleOwner(initialState: .destroyed) 152 | let mockObserver = MockLiveDataObserver() 153 | let handle = liveData.observe(owner: mockOwner, observer: mockObserver.observer) 154 | defer { liveData.removeObserver(handle: handle) } 155 | 156 | // Actions 157 | liveData.value = 1 158 | liveData.value = nil 159 | liveData.value = 2 160 | liveData.value = 3 161 | liveData.value = 5 162 | 163 | // Assertions 164 | XCTAssertEqual(0, mockObserver.valuesObserved.count) 165 | } 166 | 167 | func testObserverNotifiedOfChangeWhenLifecycleInactiveWhenLifecycleTransitionsToStarted() { 168 | // Setup 169 | 170 | let liveData = MutableLiveData(initialValue: 0) 171 | let mockOwner = MockLifecycleOwner(initialState: .created) 172 | let mockObserver = MockLiveDataObserver() 173 | let handle = liveData.observe(owner: mockOwner, observer: mockObserver.observer) 174 | defer { liveData.removeObserver(handle: handle) } 175 | 176 | // Actions 177 | liveData.value = 123 178 | mockOwner.transitionToState(.started) 179 | 180 | // Assertions 181 | 182 | guard 1 == mockObserver.valuesObserved.count else { 183 | XCTFail("Expected to be notified 1 time of value changes: \(mockObserver.valuesObserved.count)") 184 | return 185 | } 186 | 187 | XCTAssertEqual(123, mockObserver.valuesObserved[0]) 188 | } 189 | 190 | func test_GivenLifecycleInactive_WhenWhenLifecycleTransitionsToStarted_ThenObserverOnlyNotifiedOfLatestChange() { 191 | // Setup 192 | 193 | let liveData = MutableLiveData(initialValue: 0) 194 | let mockOwner = MockLifecycleOwner(initialState: .created) 195 | let mockObserver = MockLiveDataObserver() 196 | let handle = liveData.observe(owner: mockOwner, observer: mockObserver.observer) 197 | defer { liveData.removeObserver(handle: handle) } 198 | 199 | // Actions 200 | liveData.value = 1 201 | liveData.value = 2 202 | liveData.value = 3 203 | liveData.value = 4 204 | liveData.value = 5 205 | mockOwner.transitionToState(.started) 206 | 207 | // Assertions 208 | 209 | guard 1 == mockObserver.valuesObserved.count else { 210 | XCTFail("Expected to be notified 1 time of value changes: \(mockObserver.valuesObserved.count)") 211 | return 212 | } 213 | 214 | XCTAssertEqual(5, mockObserver.valuesObserved[0]) 215 | } 216 | 217 | func testObserverNotifiedOfPendingChangesOnlyWhenItsLifecycleOwnerBecomesActive() { 218 | // Setup 219 | 220 | let liveData = MutableLiveData(initialValue: 0) 221 | 222 | let mockOwnerA = MockLifecycleOwner(initialState: .created) 223 | let mockObserverA = MockLiveDataObserver() 224 | let handleA = liveData.observe(owner: mockOwnerA, observer: mockObserverA.observer) 225 | defer { 226 | liveData.removeObserver(handle: handleA) 227 | 228 | } 229 | 230 | let mockOwnerB = MockLifecycleOwner(initialState: .created) 231 | let mockObserverB = MockLiveDataObserver() 232 | let handleB = liveData.observe(owner: mockOwnerB, observer: mockObserverB.observer) 233 | defer { 234 | liveData.removeObserver(handle: handleB) 235 | 236 | } 237 | 238 | // Actions 239 | 240 | liveData.value = 123 241 | mockOwnerA.transitionToState(.started) 242 | 243 | // Assertions 244 | 245 | XCTAssertEqual(1, mockObserverA.valuesObserved.count) 246 | XCTAssertEqual(0, mockObserverB.valuesObserved.count) 247 | } 248 | 249 | func test_GivenLifecycleStarted_WhenValueChangedAndLifecycleStopsAndLifecycleStarts_ThenValueShouldNotBeSentAgain() { 250 | // Given lifecycle owner in an active state 251 | let mockOwner = MockLifecycleOwner(initialState: .started) 252 | 253 | // And an live data observer for that lifecycle owner 254 | let liveData = MutableLiveData(initialValue: 0) 255 | let mockObserver = MockLiveDataObserver() 256 | let handle = liveData.observe(owner: mockOwner, observer: mockObserver.observer) 257 | defer { liveData.removeObserver(handle: handle) } 258 | 259 | // When the live data value is changed 260 | liveData.value = 1 261 | 262 | // And the lifecycle owner becomes inactive 263 | mockOwner.transitionToState(.created) 264 | 265 | // And the lifecycle owner becomes active again 266 | mockOwner.transitionToState(.started) 267 | 268 | // Then the live data observer should have received one notification 269 | guard 1 == mockObserver.valuesObserved.count else { 270 | XCTFail("Expected to be notified 1 time of value changes: \(mockObserver.valuesObserved.count)") 271 | return 272 | } 273 | 274 | // And the live data observer should have been notified of the correct value 275 | XCTAssertEqual([1], mockObserver.valuesObserved) 276 | } 277 | 278 | // MARK: Test notifications not sent for equal value 279 | 280 | func test_WhenEqualValuesSetConsecutively_ThenObserverNotifiedOnce() { 281 | // Given lifecycle owner in an active state 282 | let mockOwner = MockLifecycleOwner(initialState: .started) 283 | 284 | // And an live data observer for that lifecycle owner 285 | let liveData = MutableLiveData(initialValue: 0) 286 | let mockObserver = MockLiveDataObserver() 287 | let handle = liveData.observe(owner: mockOwner, observer: mockObserver.observer) 288 | defer { liveData.removeObserver(handle: handle) } 289 | 290 | // When the live data value is set to equal values on consecutive calls 291 | liveData.value = 1 292 | liveData.value = 1 293 | liveData.value = 1 294 | 295 | // Then the live data observer should only be notified once 296 | guard 1 == mockObserver.valuesObserved.count else { 297 | XCTFail("Expected to be notified 1 time of value changes: \(mockObserver.valuesObserved.count)") 298 | return 299 | } 300 | 301 | // And the live data observer should have been notified of the correct value 302 | XCTAssertEqual([1], mockObserver.valuesObserved) 303 | } 304 | 305 | } 306 | -------------------------------------------------------------------------------- /Tests/ArchitectureComponentsTests/LiveData/LiveData_ObserverManagementTests.swift: -------------------------------------------------------------------------------- 1 | // ArchitectureComponents 2 | // Copyright (c) 2017 SPRI, LLC . Some rights reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | import XCTest 17 | @testable import ArchitectureComponents 18 | 19 | 20 | class LiveData_ObserverManagementTests: XCTestCase { 21 | 22 | func testInitialState() { 23 | // Action 24 | 25 | let liveData = MutableLiveData(initialValue: 0) 26 | 27 | // Assertions 28 | 29 | XCTAssertFalse(liveData.hasObservers) 30 | XCTAssertFalse(liveData.hasActiveObservers) 31 | } 32 | 33 | func testObserveForever() { 34 | // Setup 35 | 36 | let liveData = MutableLiveData(initialValue: 0) 37 | let mockObserver = MockLiveDataObserver() 38 | 39 | // Action 40 | 41 | let handle = liveData.observeForever(observer: mockObserver.observer) 42 | defer { liveData.removeObserver(handle: handle) } 43 | 44 | // Assertions 45 | 46 | XCTAssertTrue(liveData.hasObservers) 47 | XCTAssertTrue(liveData.hasActiveObservers) 48 | } 49 | 50 | func testObserve_InactiveLifecycle() { 51 | // Setup 52 | 53 | let liveData = MutableLiveData(initialValue: 0) 54 | let mockObserver = MockLiveDataObserver() 55 | let lifecycleOwner = MockLifecycleOwner(initialState: .created) 56 | 57 | // Action 58 | 59 | let handle = liveData.observe(owner: lifecycleOwner, observer: mockObserver.observer) 60 | defer { liveData.removeObserver(handle: handle) } 61 | 62 | // Assertions 63 | 64 | XCTAssertTrue(liveData.hasObservers) 65 | 66 | XCTAssertFalse(liveData.hasActiveObservers) 67 | } 68 | 69 | func testObserve_ActiveLifecycle() { 70 | // Setup 71 | 72 | let liveData = MutableLiveData(initialValue: 0) 73 | let mockObserver = MockLiveDataObserver() 74 | let lifecycleOwner = MockLifecycleOwner(initialState: .started) 75 | 76 | // Action 77 | 78 | let handle = liveData.observe(owner: lifecycleOwner, observer: mockObserver.observer) 79 | defer { liveData.removeObserver(handle: handle) } 80 | 81 | // Assertions 82 | 83 | XCTAssertTrue(liveData.hasObservers) 84 | XCTAssertTrue(liveData.hasActiveObservers) 85 | } 86 | 87 | func testRemoveObserver() { 88 | // Setup 89 | 90 | let liveData = MutableLiveData(initialValue: 0) 91 | let mockObserver = MockLiveDataObserver() 92 | 93 | let handle = liveData.observeForever(observer: mockObserver.observer) 94 | 95 | // Action 96 | 97 | liveData.removeObserver(handle: handle) 98 | 99 | // Assertions 100 | 101 | XCTAssertFalse(liveData.hasObservers) 102 | XCTAssertFalse(liveData.hasActiveObservers) 103 | } 104 | 105 | } 106 | -------------------------------------------------------------------------------- /Tests/ArchitectureComponentsTests/LiveData/MutableLiveData_PostValueTests.swift: -------------------------------------------------------------------------------- 1 | // ArchitectureComponents 2 | // Copyright (c) 2017 SPRI, LLC . Some rights reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | import XCTest 17 | @testable import ArchitectureComponents 18 | 19 | 20 | class MutableLiveData_ObserverManagementTests: XCTestCase { 21 | 22 | func testFromMainQueuePostedValuesFollowSetValues() { 23 | // Setup 24 | 25 | let liveData = MutableLiveData(initialValue: 0) 26 | 27 | let mockObserver = MockLiveDataObserver() 28 | let handle = liveData.observeForever(observer: mockObserver.observer) 29 | defer { liveData.removeObserver(handle: handle) } 30 | 31 | // Actions 32 | 33 | liveData.postValue(1) 34 | liveData.value = 2 35 | 36 | allowQueuedBlocksForMainToProcess() 37 | 38 | // Assertions 39 | 40 | XCTAssertEqual([2, 1], mockObserver.valuesObserved) 41 | } 42 | 43 | func testMultipleUndispatchedCallsToPostOnlyResultInOneValueChange() { 44 | // Setup 45 | 46 | let liveData = MutableLiveData(initialValue: 0) 47 | 48 | let mockObserver = MockLiveDataObserver() 49 | let handle = liveData.observeForever(observer: mockObserver.observer) 50 | defer { liveData.removeObserver(handle: handle) } 51 | 52 | // Actions 53 | 54 | liveData.postValue(1) 55 | liveData.postValue(2) 56 | liveData.postValue(3) 57 | liveData.postValue(4) 58 | liveData.postValue(5) 59 | 60 | allowQueuedBlocksForMainToProcess() 61 | 62 | // Assertions 63 | 64 | XCTAssertEqual([5], mockObserver.valuesObserved) 65 | } 66 | 67 | // MARK: Private (Helpers) 68 | 69 | func allowQueuedBlocksForMainToProcess() { 70 | let expectation = self.expectation(description: "Allow queued blocks for main queue to be processed") 71 | DispatchQueue.global().async { 72 | DispatchQueue.main.async { 73 | expectation.fulfill() 74 | } 75 | } 76 | self.waitForExpectations(timeout: 0.1) 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /Tests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import ArchitectureComponentsTests 3 | 4 | XCTMain([ 5 | // testCase(ArchitectureComponentsTests.allTests), 6 | ]) 7 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | if ! command -v carthage > /dev/null; then 4 | printf 'Carthage is not installed.\n' 5 | printf 'See https://github.com/Carthage/Carthage for install instructions.\n' 6 | exit 1 7 | fi 8 | 9 | carthage update --platform iOS --use-submodules --no-use-binaries 10 | 11 | --------------------------------------------------------------------------------