├── .github └── workflows │ └── docc.yml ├── .gitignore ├── .swiftlint.yml ├── .swiftpm └── xcode │ ├── package.xcworkspace │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ └── xcschemes │ ├── AppContainer.xcscheme │ └── AppContainerUI.xcscheme ├── AppContainer.docc ├── AppContainer.md ├── AppContainerUI.md └── Resources │ └── concept.png ├── AppContainer.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ ├── IDEWorkspaceChecks.plist │ └── swiftpm │ └── Package.resolved ├── Example ├── Example.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── Example │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Base.lproj │ │ └── LaunchScreen.storyboard │ ├── Example.entitlements │ ├── Extension │ │ ├── AppContainer.swift │ │ ├── UIApplication.swift │ │ └── UserDefaults.swift │ ├── Info.plist │ ├── SceneDelegate.swift │ ├── TransitionPresenter.swift │ ├── View │ │ └── KeyValueTableViewCell.swift │ └── ViewController.swift ├── ExampleTests │ └── ExampleTests.swift ├── ExampleUITests │ ├── ExampleUITests.swift │ └── ExampleUITestsLaunchTests.swift └── Package.swift ├── LICENSE ├── Makefile ├── Package.resolved ├── Package.swift ├── README.ja.md ├── README.md ├── Sources ├── AppContainer │ ├── AppContainer.swift │ ├── AppContainerDelegate.swift │ ├── AppContainerError.swift │ ├── Constants.swift │ ├── Extension │ │ ├── Array.swift │ │ ├── FileManager.swift │ │ └── UUID.swift │ ├── Model │ │ ├── AppContainerSettings.swift │ │ └── Container.swift │ ├── Notification.swift │ └── Util │ │ └── WeakHashTable.swift └── AppContainerUI │ ├── ContainerInfoView.swift │ ├── ContainerListView.swift │ ├── Extension │ └── SwiftUI │ │ └── View+.swift │ ├── UIKit │ ├── ContainerInfoViewController.swift │ └── ContainerListViewController.swift │ └── View │ ├── ContainerRowView.swift │ ├── KeyValueRowView.swift │ └── WritableKeyValueRowView.swift ├── Tests └── AppContainerTests │ └── AppContainerTests.swift └── scripts ├── docc-preview.sh ├── docc.sh └── generate-symbols.sh /.github/workflows/docc.yml: -------------------------------------------------------------------------------- 1 | name: DocC 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | build: 8 | name: Generate DocC 9 | runs-on: macos-13 10 | steps: 11 | - uses: actions/checkout@v3 12 | 13 | - name: Select Xcode 15 14 | run: sudo xcode-select -s /Applications/Xcode_15.0.app 15 | 16 | - name: Build DocC 17 | run: | 18 | make docc 19 | 20 | - uses: actions/upload-pages-artifact@v2 21 | with: 22 | path: docs 23 | 24 | deploy: 25 | needs: build 26 | permissions: 27 | pages: write 28 | id-token: write 29 | environment: 30 | name: github-pages 31 | url: ${{ steps.deployment.outputs.page_url }} 32 | runs-on: macos-13 33 | steps: 34 | - name: Deploy to GitHub Pages 35 | id: deployment 36 | uses: actions/deploy-pages@v2 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | DerivedData/ 5 | .swiftpm/config/registries.json 6 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 7 | .netrc 8 | 9 | ## Build generated 10 | build/ 11 | DerivedData/ 12 | 13 | ## Various settings 14 | *.pbxuser 15 | !default.pbxuser 16 | *.mode1v3 17 | !default.mode1v3 18 | *.mode2v3 19 | !default.mode2v3 20 | *.perspectivev3 21 | !default.perspectivev3 22 | xcuserdata/ 23 | node_modules/ 24 | 25 | ## Other 26 | *.moved-aside 27 | *.xccheckout 28 | *.xcscmblueprint 29 | *.DS_Store 30 | 31 | ## Obj-C/Swift specific 32 | *.hmap 33 | *.ipa 34 | *.dSYM.zip 35 | *.dSYM 36 | *.generated.swift 37 | 38 | 39 | Carthage/ 40 | Pods/ 41 | 42 | # CocoaPods 43 | # 44 | # We recommend against adding the Pods directory to your .gitignore. However 45 | # you should judge for yourself, the pros and cons are mentioned at: 46 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 47 | # 48 | 49 | docs 50 | symbol-graphs 51 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | #- block_based_kvo 3 | #- class_delegate_protocol 4 | #- closing_brace 5 | #- closure_parameter_position 6 | #- colon 7 | #- comma 8 | #- comment_spacing 9 | #- compiler_protocol_init 10 | #- computed_accessors_order 11 | #- control_statement 12 | #- custom_rules 13 | #- cyclomatic_complexity 14 | #- deployment_target 15 | #- discouraged_direct_init 16 | #- duplicate_enum_cases 17 | #- duplicate_imports 18 | #- duplicated_key_in_dictionary_literal 19 | #- dynamic_inline 20 | #- empty_enum_arguments 21 | #- empty_parameters 22 | #- empty_parentheses_with_trailing_closure 23 | #- file_length 24 | #- for_where 25 | - force_cast 26 | #- force_try 27 | #- function_body_length 28 | #- function_parameter_count 29 | #- generic_type_name 30 | #- identifier_name 31 | #- implicit_getter 32 | #- inclusive_language 33 | #- inert_defer 34 | #- is_disjoint 35 | #- large_tuple 36 | #- leading_whitespace 37 | #- legacy_cggeometry_functions 38 | #- legacy_constant 39 | #- legacy_constructor 40 | #- legacy_hashing 41 | #- legacy_nsgeometry_functions 42 | #- legacy_random 43 | #- line_length 44 | #- mark 45 | #- multiple_closures_with_trailing_closure 46 | #- nesting 47 | #- no_fallthrough_only 48 | #- no_space_in_method_call 49 | #- notification_center_detachment 50 | #- nsobject_prefer_isequal 51 | #- opening_brace 52 | #- operator_whitespace 53 | #- orphaned_doc_comment 54 | #- private_over_fileprivate 55 | #- private_unit_test 56 | #- protocol_property_accessors_order 57 | #- reduce_boolean 58 | #- redundant_discardable_let 59 | #- redundant_objc_attribute 60 | #- redundant_optional_initialization 61 | #- redundant_set_access_control 62 | #- redundant_string_enum_value 63 | #- redundant_void_return 64 | #- return_arrow_whitespace 65 | #- self_in_property_initialization 66 | #- shorthand_operator 67 | #- statement_position 68 | #- superfluous_disable_command 69 | #- switch_case_alignment 70 | #- syntactic_sugar 71 | #- todo 72 | #- trailing_comma 73 | #- trailing_newline 74 | #- trailing_semicolon 75 | #- trailing_whitespace 76 | #- type_body_length 77 | - type_name 78 | #- unavailable_condition 79 | #- unneeded_break_in_switch 80 | #- unused_capture_list 81 | #- unused_closure_parameter 82 | #- unused_control_flow_label 83 | #- unused_enumerated 84 | #- unused_optional_binding 85 | #- unused_setter_value 86 | #- valid_ibinspectable 87 | #- vertical_parameter_alignment 88 | #- vertical_whitespace 89 | #- void_function_in_ternary 90 | #- void_return 91 | #- xctfail_message 92 | 93 | 94 | opt_in_rules: 95 | - accessibility_label_for_image 96 | #- anonymous_argument_in_multiline_closure 97 | - anyobject_protocol 98 | - array_init 99 | - attributes 100 | - balanced_xctest_lifecycle 101 | - capture_variable 102 | - closure_body_length 103 | - closure_end_indentation 104 | - closure_spacing 105 | - collection_alignment 106 | - comma_inheritance 107 | #- conditional_returns_on_newline 108 | - contains_over_filter_count 109 | - contains_over_filter_is_empty 110 | - contains_over_first_not_nil 111 | - contains_over_range_nil_comparison 112 | - convenience_type 113 | - discarded_notification_center_observer 114 | - discouraged_assert 115 | - discouraged_none_name 116 | #- discouraged_object_literal 117 | - discouraged_optional_boolean 118 | - discouraged_optional_collection 119 | - empty_collection_literal 120 | - empty_count 121 | - empty_string 122 | - empty_xctest_method 123 | - enum_case_associated_values_count 124 | - expiring_todo 125 | #- explicit_acl 126 | #- explicit_enum_raw_value 127 | - explicit_init 128 | #- explicit_self 129 | #- explicit_top_level_acl 130 | #- explicit_type_interface 131 | #- extension_access_modifier 132 | - fallthrough 133 | - fatal_error_message 134 | #- file_header 135 | - file_name 136 | - file_name_no_space 137 | - file_types_order 138 | - first_where 139 | - flatmap_over_map_reduce 140 | - force_unwrapping 141 | - function_default_parameter_at_end 142 | - ibinspectable_in_extension 143 | - identical_operands 144 | - implicit_return 145 | #- implicitly_unwrapped_optional 146 | #- indentation_width 147 | - joined_default_parameter 148 | - last_where 149 | - legacy_multiple 150 | #- legacy_objc_type 151 | - let_var_whitespace 152 | - literal_expression_end_indentation 153 | - lower_acl_than_parent 154 | - missing_docs 155 | - modifier_order 156 | #- multiline_arguments 157 | #- multiline_arguments_brackets 158 | #- multiline_function_chains 159 | #- multiline_literal_brackets 160 | #- multiline_parameters 161 | #- multiline_parameters_brackets 162 | - nimble_operator 163 | #- no_extension_access_modifier 164 | #- no_grouping_extension 165 | - nslocalizedstring_key 166 | - nslocalizedstring_require_bundle 167 | #- number_separator 168 | #- object_literal 169 | - operator_usage_whitespace 170 | - optional_enum_case_matching 171 | - overridden_super_call 172 | - override_in_extension 173 | - pattern_matching_keywords 174 | - prefer_nimble 175 | - prefer_self_in_static_references 176 | - prefer_self_type_over_type_of_self 177 | - prefer_zero_over_explicit_init 178 | #- prefixed_toplevel_constant 179 | - private_action 180 | - private_outlet 181 | - private_subject 182 | #- prohibited_interface_builder 183 | - prohibited_super_call 184 | - quick_discouraged_call 185 | - quick_discouraged_focused_test 186 | - quick_discouraged_pending_test 187 | - raw_value_for_camel_cased_codable_enum 188 | - reduce_into 189 | - redundant_nil_coalescing 190 | - redundant_type_annotation 191 | #- required_deinit 192 | - required_enum_case 193 | - return_value_from_void_function 194 | - self_binding 195 | - single_test_class 196 | - sorted_first_last 197 | #- sorted_imports 198 | - static_operator 199 | - strict_fileprivate 200 | #- strong_iboutlet 201 | - switch_case_on_newline 202 | - test_case_accessibility 203 | - toggle_bool 204 | #- trailing_closure 205 | - type_contents_order 206 | - typesafe_array_init 207 | - unavailable_function 208 | - unneeded_parentheses_in_closure_argument 209 | - unowned_variable_capture 210 | - untyped_error_in_catch 211 | - unused_declaration 212 | - unused_import 213 | - vertical_parameter_alignment_on_call 214 | #- vertical_whitespace_between_cases 215 | #- vertical_whitespace_closing_braces 216 | #- vertical_whitespace_opening_braces 217 | - weak_delegate 218 | - xct_specific_matcher 219 | - yoda_condition 220 | 221 | excluded: 222 | - Pods 223 | - Carthage 224 | - SourcePackages 225 | - Generated 226 | - BuildTools 227 | - Example 228 | 229 | line_length: 230 | warning: 300 231 | error: 500 232 | 233 | identifier_name: 234 | min_length: 235 | warning: 1 236 | excluded: 237 | - id 238 | - URL 239 | - GlobalAPIKey 240 | 241 | file_name: 242 | excluded: 243 | - main.swift 244 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/AppContainer.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 42 | 43 | 49 | 50 | 56 | 57 | 58 | 59 | 61 | 62 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/AppContainerUI.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 42 | 43 | 49 | 50 | 56 | 57 | 58 | 59 | 61 | 62 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /AppContainer.docc/AppContainer.md: -------------------------------------------------------------------------------- 1 | # ``AppContainer`` 2 | 3 | 🧳 Create and manage multiple environments within a single app. 4 | 5 | @Metadata { 6 | @DocumentationExtension(mergeBehavior: append) 7 | @Available(iOS, introduced: "14.0") 8 | } 9 | 10 | ## Overview 11 | 12 | It is a library that allows you to create and manage multiple environments with a single application, just as if you were replacing containers. 13 | You can switch environments without deleting the application. 14 | 15 | ### Concept 16 | 17 | Normally there is one environment (Directory, UserDefaults, Cookies, Cache, ...) for one app. 18 | This means that to prepare multiple environments, multiple installations of the same application are required. 19 | For apps with login capabilities, you may need to sign in and sign out repeatedly each time to use multiple accounts. 20 | 21 | So we wondered if it would be possible to create multiple environments within the same application and switch between them easily. 22 | We created this library called AppContainer. 23 | 24 | ![Concept Image](concept.png) 25 | 26 | ### Usage 27 | 28 | #### AppGroup 29 | 30 | ```swift 31 | extension AppContainer { 32 | static let group = .init(groupIdentifier: "YOUR APP GROUP IDENTIFIER") 33 | } 34 | ``` 35 | 36 | #### Methods 37 | 38 | - Create New Container 39 | ```swift 40 | let container = try AppContainer.standard.createNewContainer(name: "Debug1") 41 | ``` 42 | 43 | - Get Container List 44 | ```swift 45 | let containers: [Container] = AppContainer.standard.containers 46 | ``` 47 | 48 | - Get Active Container 49 | ```swift 50 | let activeContainer: Container? = AppContainer.standard.activeContainer 51 | ``` 52 | 53 | - Activate Contrainer 54 | It is recommended to restart the application after calling this method. 55 | ```swift 56 | try AppContainer.standard.activate(container: container) 57 | ``` 58 | ```swift 59 | try AppContainer.standard.activateContainer(uuid: uuid) 60 | ``` 61 | 62 | - Delete Container 63 | If the container you are deleting is in use, activate the Default container before deleting it. 64 | ```swift 65 | try AppContainer.standard.delete(container: container) 66 | ``` 67 | ```swift 68 | try AppContainer.standard.deleteContainer(uuid: uuid) 69 | ``` 70 | 71 | - Clean Container 72 | ```swift 73 | try AppContainer.standard.clean(container: container) 74 | ``` 75 | ```swift 76 | try AppContainer.standard.cleanContainer(uuid: uuid) 77 | ``` 78 | 79 | - Reset Container 80 | Revert to the state before this library was used. 81 | Specifically, the DEFAULT container will be enabled and all other AppContainer-related files will be removed. 82 | ```swift 83 | try AppContainer.standard.reset() 84 | ``` 85 | 86 | #### Notification 87 | 88 | You can receive notifications when switching containers. 89 | If you want to add additional processing to be done strictly before and after the switch, use delegate as described below. 90 | 91 | - containerWillChangeNotification: 92 | Before container switching 93 | - containerDidChangeNotification: 94 | After container change 95 | 96 | #### Delegate 97 | 98 | Delegate can be used to add optional processing when switching containers. 99 | The actions are performed in the following order. 100 | 101 | ``` swift 102 | // the `activate` method is called 103 | 104 | // ↓↓↓↓↓↓↓↓↓↓ 105 | 106 | 107 | func appContainer(_ appContainer: AppContainer, willChangeTo toContainer: Container, from fromContainer: Container?) // Delegate(before container switch) 108 | 109 | // ↓↓↓↓↓↓↓↓↓↓ 110 | 111 | // Container switching process (library) 112 | 113 | // ↓↓↓↓↓↓↓↓↓↓ 114 | 115 | func appContainer(_ appContainer: AppContainer, didChangeTo toContainer: Container, from fromContainer: Container?) // Delegate (after container switch) 116 | ``` 117 | 118 | #### Set files not to be moved when switching containers 119 | 120 | When switching containers, almost all files except for some system files are saved and restored to the container directory. 121 | You can set files to be excluded from these moves. 122 | 123 | For example, the following is an example of a case where you want to use UserDefault commonly in all containers. 124 | This file will not be saved or restored when switching containers. 125 | 126 | ```swift 127 | appcontainer.customExcludeFiles = [ 128 | "Library/Preferences/.plist" 129 | ] 130 | ``` 131 | 132 | All file paths that end with the contents of customExcludeFiles will be excluded from the move. 133 | For example, the following configuration will exclude the file named `XXX.yy` under all directories. 134 | 135 | ```swift 136 | appcontainer.customExcludeFiles = [ 137 | "XXX.yy" 138 | ] 139 | ``` 140 | 141 | ## Topics 142 | 143 | ### Group 144 | 145 | - ``Symbol`` 146 | -------------------------------------------------------------------------------- /AppContainer.docc/AppContainerUI.md: -------------------------------------------------------------------------------- 1 | # ``AppContainerUI`` 2 | 3 | 📱 UI implementation for operating ``AppContainer``. 4 | 5 | @Metadata { 6 | @DocumentationExtension(mergeBehavior: append) 7 | @Available(iOS, introduced: "14.0") 8 | } 9 | 10 | ## Overview 11 | 12 | This library is a collection of UI implementations for operating ``AppContainer``. 13 | Both SwiftUI and UIKit are supported. 14 | 15 | ## Topics 16 | 17 | ### SwiftUI 18 | 19 | - ``ContainerListView`` 20 | - ``ContainerInfoView`` 21 | 22 | ### UIKit 23 | 24 | - ``ContainerListViewController`` 25 | - ``ContainerInfoViewController`` 26 | 27 | ### Group 28 | 29 | - ``Symbol`` 30 | -------------------------------------------------------------------------------- /AppContainer.docc/Resources/concept.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p-x9/AppContainer/f5745799c10b086d29ed69f21d40fbb70767a427/AppContainer.docc/Resources/concept.png -------------------------------------------------------------------------------- /AppContainer.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /AppContainer.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /AppContainer.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "editvalueview", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/p-x9/EditValueView.git", 7 | "state" : { 8 | "revision" : "4b28fb2e370f3e18a52e25a0fa5e50dd33fbb7fc", 9 | "version" : "0.4.0" 10 | } 11 | }, 12 | { 13 | "identity" : "keypathvalue", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/p-x9/KeyPathValue.git", 16 | "state" : { 17 | "revision" : "6aeb41d6c5564ae1f18e8b7a4bc5733d739558e3", 18 | "version" : "0.0.1" 19 | } 20 | }, 21 | { 22 | "identity" : "swift-collections", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/apple/swift-collections.git", 25 | "state" : { 26 | "revision" : "937e904258d22af6e447a0b72c0bc67583ef64a2", 27 | "version" : "1.0.4" 28 | } 29 | }, 30 | { 31 | "identity" : "swiftuicolor", 32 | "kind" : "remoteSourceControl", 33 | "location" : "https://github.com/p-x9/SwiftUIColor.git", 34 | "state" : { 35 | "revision" : "61238f7460a04314dc059df68a1aa4c4b7dcb5df", 36 | "version" : "0.3.0" 37 | } 38 | } 39 | ], 40 | "version" : 2 41 | } 42 | -------------------------------------------------------------------------------- /Example/Example.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 55; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 4281B59228FDD96400602D3F /* AppContainerUI in Frameworks */ = {isa = PBXBuildFile; productRef = 4281B59128FDD96400602D3F /* AppContainerUI */; }; 11 | 42AED7A928CD12EB002D885B /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42AED7A828CD12EB002D885B /* AppDelegate.swift */; }; 12 | 42AED7AB28CD12EB002D885B /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42AED7AA28CD12EB002D885B /* SceneDelegate.swift */; }; 13 | 42AED7AD28CD12EB002D885B /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42AED7AC28CD12EB002D885B /* ViewController.swift */; }; 14 | 42AED7B228CD12ED002D885B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 42AED7B128CD12ED002D885B /* Assets.xcassets */; }; 15 | 42AED7B528CD12ED002D885B /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 42AED7B328CD12ED002D885B /* LaunchScreen.storyboard */; }; 16 | 42AED7C028CD12ED002D885B /* ExampleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42AED7BF28CD12ED002D885B /* ExampleTests.swift */; }; 17 | 42AED7CA28CD12ED002D885B /* ExampleUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42AED7C928CD12ED002D885B /* ExampleUITests.swift */; }; 18 | 42AED7CC28CD12ED002D885B /* ExampleUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42AED7CB28CD12ED002D885B /* ExampleUITestsLaunchTests.swift */; }; 19 | 42AED7DC28CD14B0002D885B /* AppContainer in Frameworks */ = {isa = PBXBuildFile; productRef = 42AED7DB28CD14B0002D885B /* AppContainer */; }; 20 | 42AED7DF28CD155A002D885B /* AppContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42AED7DE28CD155A002D885B /* AppContainer.swift */; }; 21 | 42AED7E228CD1B50002D885B /* KeyValueTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42AED7E128CD1B50002D885B /* KeyValueTableViewCell.swift */; }; 22 | 42AED7E528D0A56B002D885B /* OrderedCollections in Frameworks */ = {isa = PBXBuildFile; productRef = 42AED7E428D0A56B002D885B /* OrderedCollections */; }; 23 | 42D7D2D528EB14B800781045 /* TransitionPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42D7D2D428EB14B800781045 /* TransitionPresenter.swift */; }; 24 | 42D7D2D728EB14F500781045 /* UIApplication.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42D7D2D628EB14F500781045 /* UIApplication.swift */; }; 25 | 42DAFE3828D8AF2400FA8847 /* UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42DAFE3728D8AF2400FA8847 /* UserDefaults.swift */; }; 26 | /* End PBXBuildFile section */ 27 | 28 | /* Begin PBXContainerItemProxy section */ 29 | 42AED7BC28CD12ED002D885B /* PBXContainerItemProxy */ = { 30 | isa = PBXContainerItemProxy; 31 | containerPortal = 42AED79D28CD12EA002D885B /* Project object */; 32 | proxyType = 1; 33 | remoteGlobalIDString = 42AED7A428CD12EB002D885B; 34 | remoteInfo = Example; 35 | }; 36 | 42AED7C628CD12ED002D885B /* PBXContainerItemProxy */ = { 37 | isa = PBXContainerItemProxy; 38 | containerPortal = 42AED79D28CD12EA002D885B /* Project object */; 39 | proxyType = 1; 40 | remoteGlobalIDString = 42AED7A428CD12EB002D885B; 41 | remoteInfo = Example; 42 | }; 43 | /* End PBXContainerItemProxy section */ 44 | 45 | /* Begin PBXFileReference section */ 46 | 42AED7A528CD12EB002D885B /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; 47 | 42AED7A828CD12EB002D885B /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 48 | 42AED7AA28CD12EB002D885B /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 49 | 42AED7AC28CD12EB002D885B /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 50 | 42AED7B128CD12ED002D885B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 51 | 42AED7B428CD12ED002D885B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 52 | 42AED7B628CD12ED002D885B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 53 | 42AED7BB28CD12ED002D885B /* ExampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ExampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 54 | 42AED7BF28CD12ED002D885B /* ExampleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleTests.swift; sourceTree = ""; }; 55 | 42AED7C528CD12ED002D885B /* ExampleUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ExampleUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 56 | 42AED7C928CD12ED002D885B /* ExampleUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleUITests.swift; sourceTree = ""; }; 57 | 42AED7CB28CD12ED002D885B /* ExampleUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleUITestsLaunchTests.swift; sourceTree = ""; }; 58 | 42AED7D928CD149E002D885B /* AppContainer */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = AppContainer; path = ..; sourceTree = ""; }; 59 | 42AED7DE28CD155A002D885B /* AppContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppContainer.swift; sourceTree = ""; }; 60 | 42AED7E028CD159E002D885B /* Example.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Example.entitlements; sourceTree = ""; }; 61 | 42AED7E128CD1B50002D885B /* KeyValueTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyValueTableViewCell.swift; sourceTree = ""; }; 62 | 42D7D2D428EB14B800781045 /* TransitionPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransitionPresenter.swift; sourceTree = ""; }; 63 | 42D7D2D628EB14F500781045 /* UIApplication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIApplication.swift; sourceTree = ""; }; 64 | 42DAFE3728D8AF2400FA8847 /* UserDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaults.swift; sourceTree = ""; }; 65 | /* End PBXFileReference section */ 66 | 67 | /* Begin PBXFrameworksBuildPhase section */ 68 | 42AED7A228CD12EB002D885B /* Frameworks */ = { 69 | isa = PBXFrameworksBuildPhase; 70 | buildActionMask = 2147483647; 71 | files = ( 72 | 42AED7E528D0A56B002D885B /* OrderedCollections in Frameworks */, 73 | 42AED7DC28CD14B0002D885B /* AppContainer in Frameworks */, 74 | 4281B59228FDD96400602D3F /* AppContainerUI in Frameworks */, 75 | ); 76 | runOnlyForDeploymentPostprocessing = 0; 77 | }; 78 | 42AED7B828CD12ED002D885B /* Frameworks */ = { 79 | isa = PBXFrameworksBuildPhase; 80 | buildActionMask = 2147483647; 81 | files = ( 82 | ); 83 | runOnlyForDeploymentPostprocessing = 0; 84 | }; 85 | 42AED7C228CD12ED002D885B /* Frameworks */ = { 86 | isa = PBXFrameworksBuildPhase; 87 | buildActionMask = 2147483647; 88 | files = ( 89 | ); 90 | runOnlyForDeploymentPostprocessing = 0; 91 | }; 92 | /* End PBXFrameworksBuildPhase section */ 93 | 94 | /* Begin PBXGroup section */ 95 | 42AED79C28CD12EA002D885B = { 96 | isa = PBXGroup; 97 | children = ( 98 | 42AED7D828CD149E002D885B /* Packages */, 99 | 42AED7A728CD12EB002D885B /* Example */, 100 | 42AED7BE28CD12ED002D885B /* ExampleTests */, 101 | 42AED7C828CD12ED002D885B /* ExampleUITests */, 102 | 42AED7A628CD12EB002D885B /* Products */, 103 | 42AED7DA28CD14B0002D885B /* Frameworks */, 104 | ); 105 | sourceTree = ""; 106 | }; 107 | 42AED7A628CD12EB002D885B /* Products */ = { 108 | isa = PBXGroup; 109 | children = ( 110 | 42AED7A528CD12EB002D885B /* Example.app */, 111 | 42AED7BB28CD12ED002D885B /* ExampleTests.xctest */, 112 | 42AED7C528CD12ED002D885B /* ExampleUITests.xctest */, 113 | ); 114 | name = Products; 115 | sourceTree = ""; 116 | }; 117 | 42AED7A728CD12EB002D885B /* Example */ = { 118 | isa = PBXGroup; 119 | children = ( 120 | 42AED7E028CD159E002D885B /* Example.entitlements */, 121 | 42AED7A828CD12EB002D885B /* AppDelegate.swift */, 122 | 42AED7AA28CD12EB002D885B /* SceneDelegate.swift */, 123 | 42AED7AC28CD12EB002D885B /* ViewController.swift */, 124 | 42D7D2D428EB14B800781045 /* TransitionPresenter.swift */, 125 | 42D7D2D828EB164300781045 /* View */, 126 | 42AED7DD28CD154A002D885B /* Extension */, 127 | 42AED7B128CD12ED002D885B /* Assets.xcassets */, 128 | 42AED7B328CD12ED002D885B /* LaunchScreen.storyboard */, 129 | 42AED7B628CD12ED002D885B /* Info.plist */, 130 | ); 131 | path = Example; 132 | sourceTree = ""; 133 | }; 134 | 42AED7BE28CD12ED002D885B /* ExampleTests */ = { 135 | isa = PBXGroup; 136 | children = ( 137 | 42AED7BF28CD12ED002D885B /* ExampleTests.swift */, 138 | ); 139 | path = ExampleTests; 140 | sourceTree = ""; 141 | }; 142 | 42AED7C828CD12ED002D885B /* ExampleUITests */ = { 143 | isa = PBXGroup; 144 | children = ( 145 | 42AED7C928CD12ED002D885B /* ExampleUITests.swift */, 146 | 42AED7CB28CD12ED002D885B /* ExampleUITestsLaunchTests.swift */, 147 | ); 148 | path = ExampleUITests; 149 | sourceTree = ""; 150 | }; 151 | 42AED7D828CD149E002D885B /* Packages */ = { 152 | isa = PBXGroup; 153 | children = ( 154 | 42AED7D928CD149E002D885B /* AppContainer */, 155 | ); 156 | name = Packages; 157 | sourceTree = ""; 158 | }; 159 | 42AED7DA28CD14B0002D885B /* Frameworks */ = { 160 | isa = PBXGroup; 161 | children = ( 162 | ); 163 | name = Frameworks; 164 | sourceTree = ""; 165 | }; 166 | 42AED7DD28CD154A002D885B /* Extension */ = { 167 | isa = PBXGroup; 168 | children = ( 169 | 42AED7DE28CD155A002D885B /* AppContainer.swift */, 170 | 42DAFE3728D8AF2400FA8847 /* UserDefaults.swift */, 171 | 42D7D2D628EB14F500781045 /* UIApplication.swift */, 172 | ); 173 | path = Extension; 174 | sourceTree = ""; 175 | }; 176 | 42D7D2D828EB164300781045 /* View */ = { 177 | isa = PBXGroup; 178 | children = ( 179 | 42AED7E128CD1B50002D885B /* KeyValueTableViewCell.swift */, 180 | ); 181 | path = View; 182 | sourceTree = ""; 183 | }; 184 | /* End PBXGroup section */ 185 | 186 | /* Begin PBXNativeTarget section */ 187 | 42AED7A428CD12EB002D885B /* Example */ = { 188 | isa = PBXNativeTarget; 189 | buildConfigurationList = 42AED7CF28CD12ED002D885B /* Build configuration list for PBXNativeTarget "Example" */; 190 | buildPhases = ( 191 | 42AED7A128CD12EB002D885B /* Sources */, 192 | 42AED7A228CD12EB002D885B /* Frameworks */, 193 | 42AED7A328CD12EB002D885B /* Resources */, 194 | ); 195 | buildRules = ( 196 | ); 197 | dependencies = ( 198 | ); 199 | name = Example; 200 | packageProductDependencies = ( 201 | 42AED7DB28CD14B0002D885B /* AppContainer */, 202 | 42AED7E428D0A56B002D885B /* OrderedCollections */, 203 | 4281B59128FDD96400602D3F /* AppContainerUI */, 204 | ); 205 | productName = Example; 206 | productReference = 42AED7A528CD12EB002D885B /* Example.app */; 207 | productType = "com.apple.product-type.application"; 208 | }; 209 | 42AED7BA28CD12ED002D885B /* ExampleTests */ = { 210 | isa = PBXNativeTarget; 211 | buildConfigurationList = 42AED7D228CD12ED002D885B /* Build configuration list for PBXNativeTarget "ExampleTests" */; 212 | buildPhases = ( 213 | 42AED7B728CD12ED002D885B /* Sources */, 214 | 42AED7B828CD12ED002D885B /* Frameworks */, 215 | 42AED7B928CD12ED002D885B /* Resources */, 216 | ); 217 | buildRules = ( 218 | ); 219 | dependencies = ( 220 | 42AED7BD28CD12ED002D885B /* PBXTargetDependency */, 221 | ); 222 | name = ExampleTests; 223 | productName = ExampleTests; 224 | productReference = 42AED7BB28CD12ED002D885B /* ExampleTests.xctest */; 225 | productType = "com.apple.product-type.bundle.unit-test"; 226 | }; 227 | 42AED7C428CD12ED002D885B /* ExampleUITests */ = { 228 | isa = PBXNativeTarget; 229 | buildConfigurationList = 42AED7D528CD12ED002D885B /* Build configuration list for PBXNativeTarget "ExampleUITests" */; 230 | buildPhases = ( 231 | 42AED7C128CD12ED002D885B /* Sources */, 232 | 42AED7C228CD12ED002D885B /* Frameworks */, 233 | 42AED7C328CD12ED002D885B /* Resources */, 234 | ); 235 | buildRules = ( 236 | ); 237 | dependencies = ( 238 | 42AED7C728CD12ED002D885B /* PBXTargetDependency */, 239 | ); 240 | name = ExampleUITests; 241 | productName = ExampleUITests; 242 | productReference = 42AED7C528CD12ED002D885B /* ExampleUITests.xctest */; 243 | productType = "com.apple.product-type.bundle.ui-testing"; 244 | }; 245 | /* End PBXNativeTarget section */ 246 | 247 | /* Begin PBXProject section */ 248 | 42AED79D28CD12EA002D885B /* Project object */ = { 249 | isa = PBXProject; 250 | attributes = { 251 | BuildIndependentTargetsInParallel = 1; 252 | LastSwiftUpdateCheck = 1340; 253 | LastUpgradeCheck = 1340; 254 | TargetAttributes = { 255 | 42AED7A428CD12EB002D885B = { 256 | CreatedOnToolsVersion = 13.4.1; 257 | }; 258 | 42AED7BA28CD12ED002D885B = { 259 | CreatedOnToolsVersion = 13.4.1; 260 | TestTargetID = 42AED7A428CD12EB002D885B; 261 | }; 262 | 42AED7C428CD12ED002D885B = { 263 | CreatedOnToolsVersion = 13.4.1; 264 | TestTargetID = 42AED7A428CD12EB002D885B; 265 | }; 266 | }; 267 | }; 268 | buildConfigurationList = 42AED7A028CD12EA002D885B /* Build configuration list for PBXProject "Example" */; 269 | compatibilityVersion = "Xcode 13.0"; 270 | developmentRegion = en; 271 | hasScannedForEncodings = 0; 272 | knownRegions = ( 273 | en, 274 | Base, 275 | ); 276 | mainGroup = 42AED79C28CD12EA002D885B; 277 | packageReferences = ( 278 | 42AED7E328D0A56B002D885B /* XCRemoteSwiftPackageReference "swift-collections" */, 279 | ); 280 | productRefGroup = 42AED7A628CD12EB002D885B /* Products */; 281 | projectDirPath = ""; 282 | projectRoot = ""; 283 | targets = ( 284 | 42AED7A428CD12EB002D885B /* Example */, 285 | 42AED7BA28CD12ED002D885B /* ExampleTests */, 286 | 42AED7C428CD12ED002D885B /* ExampleUITests */, 287 | ); 288 | }; 289 | /* End PBXProject section */ 290 | 291 | /* Begin PBXResourcesBuildPhase section */ 292 | 42AED7A328CD12EB002D885B /* Resources */ = { 293 | isa = PBXResourcesBuildPhase; 294 | buildActionMask = 2147483647; 295 | files = ( 296 | 42AED7B528CD12ED002D885B /* LaunchScreen.storyboard in Resources */, 297 | 42AED7B228CD12ED002D885B /* Assets.xcassets in Resources */, 298 | ); 299 | runOnlyForDeploymentPostprocessing = 0; 300 | }; 301 | 42AED7B928CD12ED002D885B /* Resources */ = { 302 | isa = PBXResourcesBuildPhase; 303 | buildActionMask = 2147483647; 304 | files = ( 305 | ); 306 | runOnlyForDeploymentPostprocessing = 0; 307 | }; 308 | 42AED7C328CD12ED002D885B /* Resources */ = { 309 | isa = PBXResourcesBuildPhase; 310 | buildActionMask = 2147483647; 311 | files = ( 312 | ); 313 | runOnlyForDeploymentPostprocessing = 0; 314 | }; 315 | /* End PBXResourcesBuildPhase section */ 316 | 317 | /* Begin PBXSourcesBuildPhase section */ 318 | 42AED7A128CD12EB002D885B /* Sources */ = { 319 | isa = PBXSourcesBuildPhase; 320 | buildActionMask = 2147483647; 321 | files = ( 322 | 42AED7AD28CD12EB002D885B /* ViewController.swift in Sources */, 323 | 42AED7DF28CD155A002D885B /* AppContainer.swift in Sources */, 324 | 42DAFE3828D8AF2400FA8847 /* UserDefaults.swift in Sources */, 325 | 42AED7E228CD1B50002D885B /* KeyValueTableViewCell.swift in Sources */, 326 | 42AED7A928CD12EB002D885B /* AppDelegate.swift in Sources */, 327 | 42D7D2D528EB14B800781045 /* TransitionPresenter.swift in Sources */, 328 | 42D7D2D728EB14F500781045 /* UIApplication.swift in Sources */, 329 | 42AED7AB28CD12EB002D885B /* SceneDelegate.swift in Sources */, 330 | ); 331 | runOnlyForDeploymentPostprocessing = 0; 332 | }; 333 | 42AED7B728CD12ED002D885B /* Sources */ = { 334 | isa = PBXSourcesBuildPhase; 335 | buildActionMask = 2147483647; 336 | files = ( 337 | 42AED7C028CD12ED002D885B /* ExampleTests.swift in Sources */, 338 | ); 339 | runOnlyForDeploymentPostprocessing = 0; 340 | }; 341 | 42AED7C128CD12ED002D885B /* Sources */ = { 342 | isa = PBXSourcesBuildPhase; 343 | buildActionMask = 2147483647; 344 | files = ( 345 | 42AED7CA28CD12ED002D885B /* ExampleUITests.swift in Sources */, 346 | 42AED7CC28CD12ED002D885B /* ExampleUITestsLaunchTests.swift in Sources */, 347 | ); 348 | runOnlyForDeploymentPostprocessing = 0; 349 | }; 350 | /* End PBXSourcesBuildPhase section */ 351 | 352 | /* Begin PBXTargetDependency section */ 353 | 42AED7BD28CD12ED002D885B /* PBXTargetDependency */ = { 354 | isa = PBXTargetDependency; 355 | target = 42AED7A428CD12EB002D885B /* Example */; 356 | targetProxy = 42AED7BC28CD12ED002D885B /* PBXContainerItemProxy */; 357 | }; 358 | 42AED7C728CD12ED002D885B /* PBXTargetDependency */ = { 359 | isa = PBXTargetDependency; 360 | target = 42AED7A428CD12EB002D885B /* Example */; 361 | targetProxy = 42AED7C628CD12ED002D885B /* PBXContainerItemProxy */; 362 | }; 363 | /* End PBXTargetDependency section */ 364 | 365 | /* Begin PBXVariantGroup section */ 366 | 42AED7B328CD12ED002D885B /* LaunchScreen.storyboard */ = { 367 | isa = PBXVariantGroup; 368 | children = ( 369 | 42AED7B428CD12ED002D885B /* Base */, 370 | ); 371 | name = LaunchScreen.storyboard; 372 | sourceTree = ""; 373 | }; 374 | /* End PBXVariantGroup section */ 375 | 376 | /* Begin XCBuildConfiguration section */ 377 | 42AED7CD28CD12ED002D885B /* Debug */ = { 378 | isa = XCBuildConfiguration; 379 | buildSettings = { 380 | ALWAYS_SEARCH_USER_PATHS = NO; 381 | CLANG_ANALYZER_NONNULL = YES; 382 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 383 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 384 | CLANG_ENABLE_MODULES = YES; 385 | CLANG_ENABLE_OBJC_ARC = YES; 386 | CLANG_ENABLE_OBJC_WEAK = YES; 387 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 388 | CLANG_WARN_BOOL_CONVERSION = YES; 389 | CLANG_WARN_COMMA = YES; 390 | CLANG_WARN_CONSTANT_CONVERSION = YES; 391 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 392 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 393 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 394 | CLANG_WARN_EMPTY_BODY = YES; 395 | CLANG_WARN_ENUM_CONVERSION = YES; 396 | CLANG_WARN_INFINITE_RECURSION = YES; 397 | CLANG_WARN_INT_CONVERSION = YES; 398 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 399 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 400 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 401 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 402 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 403 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 404 | CLANG_WARN_STRICT_PROTOTYPES = YES; 405 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 406 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 407 | CLANG_WARN_UNREACHABLE_CODE = YES; 408 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 409 | COPY_PHASE_STRIP = NO; 410 | DEBUG_INFORMATION_FORMAT = dwarf; 411 | ENABLE_STRICT_OBJC_MSGSEND = YES; 412 | ENABLE_TESTABILITY = YES; 413 | GCC_C_LANGUAGE_STANDARD = gnu11; 414 | GCC_DYNAMIC_NO_PIC = NO; 415 | GCC_NO_COMMON_BLOCKS = YES; 416 | GCC_OPTIMIZATION_LEVEL = 0; 417 | GCC_PREPROCESSOR_DEFINITIONS = ( 418 | "DEBUG=1", 419 | "$(inherited)", 420 | ); 421 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 422 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 423 | GCC_WARN_UNDECLARED_SELECTOR = YES; 424 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 425 | GCC_WARN_UNUSED_FUNCTION = YES; 426 | GCC_WARN_UNUSED_VARIABLE = YES; 427 | IPHONEOS_DEPLOYMENT_TARGET = 15.5; 428 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 429 | MTL_FAST_MATH = YES; 430 | ONLY_ACTIVE_ARCH = YES; 431 | SDKROOT = iphoneos; 432 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 433 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 434 | }; 435 | name = Debug; 436 | }; 437 | 42AED7CE28CD12ED002D885B /* Release */ = { 438 | isa = XCBuildConfiguration; 439 | buildSettings = { 440 | ALWAYS_SEARCH_USER_PATHS = NO; 441 | CLANG_ANALYZER_NONNULL = YES; 442 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 443 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 444 | CLANG_ENABLE_MODULES = YES; 445 | CLANG_ENABLE_OBJC_ARC = YES; 446 | CLANG_ENABLE_OBJC_WEAK = YES; 447 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 448 | CLANG_WARN_BOOL_CONVERSION = YES; 449 | CLANG_WARN_COMMA = YES; 450 | CLANG_WARN_CONSTANT_CONVERSION = YES; 451 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 452 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 453 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 454 | CLANG_WARN_EMPTY_BODY = YES; 455 | CLANG_WARN_ENUM_CONVERSION = YES; 456 | CLANG_WARN_INFINITE_RECURSION = YES; 457 | CLANG_WARN_INT_CONVERSION = YES; 458 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 459 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 460 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 461 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 462 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 463 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 464 | CLANG_WARN_STRICT_PROTOTYPES = YES; 465 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 466 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 467 | CLANG_WARN_UNREACHABLE_CODE = YES; 468 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 469 | COPY_PHASE_STRIP = NO; 470 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 471 | ENABLE_NS_ASSERTIONS = NO; 472 | ENABLE_STRICT_OBJC_MSGSEND = YES; 473 | GCC_C_LANGUAGE_STANDARD = gnu11; 474 | GCC_NO_COMMON_BLOCKS = YES; 475 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 476 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 477 | GCC_WARN_UNDECLARED_SELECTOR = YES; 478 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 479 | GCC_WARN_UNUSED_FUNCTION = YES; 480 | GCC_WARN_UNUSED_VARIABLE = YES; 481 | IPHONEOS_DEPLOYMENT_TARGET = 15.5; 482 | MTL_ENABLE_DEBUG_INFO = NO; 483 | MTL_FAST_MATH = YES; 484 | SDKROOT = iphoneos; 485 | SWIFT_COMPILATION_MODE = wholemodule; 486 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 487 | VALIDATE_PRODUCT = YES; 488 | }; 489 | name = Release; 490 | }; 491 | 42AED7D028CD12ED002D885B /* Debug */ = { 492 | isa = XCBuildConfiguration; 493 | buildSettings = { 494 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 495 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 496 | CODE_SIGN_ENTITLEMENTS = Example/Example.entitlements; 497 | CODE_SIGN_STYLE = Automatic; 498 | CURRENT_PROJECT_VERSION = 1; 499 | DEVELOPMENT_TEAM = WLCQDVKTS9; 500 | GENERATE_INFOPLIST_FILE = YES; 501 | INFOPLIST_FILE = Example/Info.plist; 502 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 503 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; 504 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 505 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 506 | IPHONEOS_DEPLOYMENT_TARGET = 15.0; 507 | LD_RUNPATH_SEARCH_PATHS = ( 508 | "$(inherited)", 509 | "@executable_path/Frameworks", 510 | ); 511 | MARKETING_VERSION = 1.0; 512 | PRODUCT_BUNDLE_IDENTIFIER = "com.p-x9.AppContainerExample"; 513 | PRODUCT_NAME = "$(TARGET_NAME)"; 514 | SWIFT_EMIT_LOC_STRINGS = YES; 515 | SWIFT_VERSION = 5.0; 516 | TARGETED_DEVICE_FAMILY = "1,2"; 517 | }; 518 | name = Debug; 519 | }; 520 | 42AED7D128CD12ED002D885B /* Release */ = { 521 | isa = XCBuildConfiguration; 522 | buildSettings = { 523 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 524 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 525 | CODE_SIGN_ENTITLEMENTS = Example/Example.entitlements; 526 | CODE_SIGN_STYLE = Automatic; 527 | CURRENT_PROJECT_VERSION = 1; 528 | DEVELOPMENT_TEAM = WLCQDVKTS9; 529 | GENERATE_INFOPLIST_FILE = YES; 530 | INFOPLIST_FILE = Example/Info.plist; 531 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 532 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; 533 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 534 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 535 | IPHONEOS_DEPLOYMENT_TARGET = 15.0; 536 | LD_RUNPATH_SEARCH_PATHS = ( 537 | "$(inherited)", 538 | "@executable_path/Frameworks", 539 | ); 540 | MARKETING_VERSION = 1.0; 541 | PRODUCT_BUNDLE_IDENTIFIER = "com.p-x9.AppContainerExample"; 542 | PRODUCT_NAME = "$(TARGET_NAME)"; 543 | SWIFT_EMIT_LOC_STRINGS = YES; 544 | SWIFT_VERSION = 5.0; 545 | TARGETED_DEVICE_FAMILY = "1,2"; 546 | }; 547 | name = Release; 548 | }; 549 | 42AED7D328CD12ED002D885B /* Debug */ = { 550 | isa = XCBuildConfiguration; 551 | buildSettings = { 552 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 553 | BUNDLE_LOADER = "$(TEST_HOST)"; 554 | CODE_SIGN_STYLE = Automatic; 555 | CURRENT_PROJECT_VERSION = 1; 556 | DEVELOPMENT_TEAM = WLCQDVKTS9; 557 | GENERATE_INFOPLIST_FILE = YES; 558 | IPHONEOS_DEPLOYMENT_TARGET = 15.5; 559 | MARKETING_VERSION = 1.0; 560 | PRODUCT_BUNDLE_IDENTIFIER = "com.p-x9.ExampleTests"; 561 | PRODUCT_NAME = "$(TARGET_NAME)"; 562 | SWIFT_EMIT_LOC_STRINGS = NO; 563 | SWIFT_VERSION = 5.0; 564 | TARGETED_DEVICE_FAMILY = "1,2"; 565 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Example.app/Example"; 566 | }; 567 | name = Debug; 568 | }; 569 | 42AED7D428CD12ED002D885B /* Release */ = { 570 | isa = XCBuildConfiguration; 571 | buildSettings = { 572 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 573 | BUNDLE_LOADER = "$(TEST_HOST)"; 574 | CODE_SIGN_STYLE = Automatic; 575 | CURRENT_PROJECT_VERSION = 1; 576 | DEVELOPMENT_TEAM = WLCQDVKTS9; 577 | GENERATE_INFOPLIST_FILE = YES; 578 | IPHONEOS_DEPLOYMENT_TARGET = 15.5; 579 | MARKETING_VERSION = 1.0; 580 | PRODUCT_BUNDLE_IDENTIFIER = "com.p-x9.ExampleTests"; 581 | PRODUCT_NAME = "$(TARGET_NAME)"; 582 | SWIFT_EMIT_LOC_STRINGS = NO; 583 | SWIFT_VERSION = 5.0; 584 | TARGETED_DEVICE_FAMILY = "1,2"; 585 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Example.app/Example"; 586 | }; 587 | name = Release; 588 | }; 589 | 42AED7D628CD12ED002D885B /* Debug */ = { 590 | isa = XCBuildConfiguration; 591 | buildSettings = { 592 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 593 | CODE_SIGN_STYLE = Automatic; 594 | CURRENT_PROJECT_VERSION = 1; 595 | DEVELOPMENT_TEAM = WLCQDVKTS9; 596 | GENERATE_INFOPLIST_FILE = YES; 597 | MARKETING_VERSION = 1.0; 598 | PRODUCT_BUNDLE_IDENTIFIER = "com.p-x9.ExampleUITests"; 599 | PRODUCT_NAME = "$(TARGET_NAME)"; 600 | SWIFT_EMIT_LOC_STRINGS = NO; 601 | SWIFT_VERSION = 5.0; 602 | TARGETED_DEVICE_FAMILY = "1,2"; 603 | TEST_TARGET_NAME = Example; 604 | }; 605 | name = Debug; 606 | }; 607 | 42AED7D728CD12ED002D885B /* Release */ = { 608 | isa = XCBuildConfiguration; 609 | buildSettings = { 610 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 611 | CODE_SIGN_STYLE = Automatic; 612 | CURRENT_PROJECT_VERSION = 1; 613 | DEVELOPMENT_TEAM = WLCQDVKTS9; 614 | GENERATE_INFOPLIST_FILE = YES; 615 | MARKETING_VERSION = 1.0; 616 | PRODUCT_BUNDLE_IDENTIFIER = "com.p-x9.ExampleUITests"; 617 | PRODUCT_NAME = "$(TARGET_NAME)"; 618 | SWIFT_EMIT_LOC_STRINGS = NO; 619 | SWIFT_VERSION = 5.0; 620 | TARGETED_DEVICE_FAMILY = "1,2"; 621 | TEST_TARGET_NAME = Example; 622 | }; 623 | name = Release; 624 | }; 625 | /* End XCBuildConfiguration section */ 626 | 627 | /* Begin XCConfigurationList section */ 628 | 42AED7A028CD12EA002D885B /* Build configuration list for PBXProject "Example" */ = { 629 | isa = XCConfigurationList; 630 | buildConfigurations = ( 631 | 42AED7CD28CD12ED002D885B /* Debug */, 632 | 42AED7CE28CD12ED002D885B /* Release */, 633 | ); 634 | defaultConfigurationIsVisible = 0; 635 | defaultConfigurationName = Release; 636 | }; 637 | 42AED7CF28CD12ED002D885B /* Build configuration list for PBXNativeTarget "Example" */ = { 638 | isa = XCConfigurationList; 639 | buildConfigurations = ( 640 | 42AED7D028CD12ED002D885B /* Debug */, 641 | 42AED7D128CD12ED002D885B /* Release */, 642 | ); 643 | defaultConfigurationIsVisible = 0; 644 | defaultConfigurationName = Release; 645 | }; 646 | 42AED7D228CD12ED002D885B /* Build configuration list for PBXNativeTarget "ExampleTests" */ = { 647 | isa = XCConfigurationList; 648 | buildConfigurations = ( 649 | 42AED7D328CD12ED002D885B /* Debug */, 650 | 42AED7D428CD12ED002D885B /* Release */, 651 | ); 652 | defaultConfigurationIsVisible = 0; 653 | defaultConfigurationName = Release; 654 | }; 655 | 42AED7D528CD12ED002D885B /* Build configuration list for PBXNativeTarget "ExampleUITests" */ = { 656 | isa = XCConfigurationList; 657 | buildConfigurations = ( 658 | 42AED7D628CD12ED002D885B /* Debug */, 659 | 42AED7D728CD12ED002D885B /* Release */, 660 | ); 661 | defaultConfigurationIsVisible = 0; 662 | defaultConfigurationName = Release; 663 | }; 664 | /* End XCConfigurationList section */ 665 | 666 | /* Begin XCRemoteSwiftPackageReference section */ 667 | 42AED7E328D0A56B002D885B /* XCRemoteSwiftPackageReference "swift-collections" */ = { 668 | isa = XCRemoteSwiftPackageReference; 669 | repositoryURL = "https://github.com/apple/swift-collections.git"; 670 | requirement = { 671 | kind = upToNextMajorVersion; 672 | minimumVersion = 1.0.0; 673 | }; 674 | }; 675 | /* End XCRemoteSwiftPackageReference section */ 676 | 677 | /* Begin XCSwiftPackageProductDependency section */ 678 | 4281B59128FDD96400602D3F /* AppContainerUI */ = { 679 | isa = XCSwiftPackageProductDependency; 680 | productName = AppContainerUI; 681 | }; 682 | 42AED7DB28CD14B0002D885B /* AppContainer */ = { 683 | isa = XCSwiftPackageProductDependency; 684 | productName = AppContainer; 685 | }; 686 | 42AED7E428D0A56B002D885B /* OrderedCollections */ = { 687 | isa = XCSwiftPackageProductDependency; 688 | package = 42AED7E328D0A56B002D885B /* XCRemoteSwiftPackageReference "swift-collections" */; 689 | productName = OrderedCollections; 690 | }; 691 | /* End XCSwiftPackageProductDependency section */ 692 | }; 693 | rootObject = 42AED79D28CD12EA002D885B /* Project object */; 694 | } 695 | -------------------------------------------------------------------------------- /Example/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/Example/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Example 4 | // 5 | // Created by p-x9 on 2022/09/11. 6 | // 7 | // 8 | 9 | import UIKit 10 | 11 | @main 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | // Override point for customization after application launch. 18 | return true 19 | } 20 | 21 | // MARK: UISceneSession Lifecycle 22 | 23 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 24 | // Called when a new scene session is being created. 25 | // Use this method to select a configuration to create the new scene with. 26 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 27 | } 28 | 29 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 30 | // Called when the user discards a scene session. 31 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 32 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 33 | } 34 | 35 | 36 | } 37 | 38 | -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "2x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "83.5x83.5" 82 | }, 83 | { 84 | "idiom" : "ios-marketing", 85 | "scale" : "1x", 86 | "size" : "1024x1024" 87 | } 88 | ], 89 | "info" : { 90 | "author" : "xcode", 91 | "version" : 1 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/Example/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Example/Example/Example.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.application-groups 6 | 7 | group.com.p-x9.AppContainerExample 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Example/Example/Extension/AppContainer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppContainer.swift 3 | // Example 4 | // 5 | // Created by p-x9 on 2022/09/11. 6 | // 7 | // 8 | 9 | import Foundation 10 | import AppContainer 11 | 12 | extension AppContainer { 13 | static let group = AppContainer(groupIdentifier: "group.com.p-x9.AppContainerExample") 14 | } 15 | -------------------------------------------------------------------------------- /Example/Example/Extension/UIApplication.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIApplication.swift 3 | // Example 4 | // 5 | // Created by p-x9 on 2022/10/03. 6 | // 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIApplication { 12 | 13 | var keyWindow: UIWindow? { 14 | if #available(iOS 13, *) { 15 | return self.connectedScenes 16 | .compactMap { $0 as? UIWindowScene } 17 | .flatMap { $0.windows } 18 | .first { $0.isKeyWindow } 19 | } else { 20 | return self.windows.first { $0.isKeyWindow } 21 | } 22 | } 23 | 24 | var topViewController: UIViewController? { 25 | return topViewController() 26 | } 27 | 28 | func topViewController(controller: UIViewController? = nil) -> UIViewController? { 29 | let controller = controller ?? self.keyWindow?.rootViewController 30 | if let navigationController = controller as? UINavigationController { 31 | return topViewController(controller: navigationController.visibleViewController) 32 | } 33 | if let tabController = controller as? UITabBarController { 34 | if let selected = tabController.selectedViewController { 35 | return topViewController(controller: selected) 36 | } 37 | } 38 | if let presented = controller?.presentedViewController { 39 | return topViewController(controller: presented) 40 | } 41 | if let alertController = controller as? UIAlertController, let presenting = alertController.presentingViewController { 42 | return presenting 43 | } 44 | return controller 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Example/Example/Extension/UserDefaults.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserDefaults.swift 3 | // Example 4 | // 5 | // Created by p-x9 on 2022/09/19. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | extension UserDefaults { 12 | enum ValueType { 13 | case int 14 | case double 15 | case string 16 | case bool 17 | case data 18 | 19 | indirect case array(ValueType) 20 | indirect case dictionary(ValueType, ValueType) 21 | 22 | case unknown 23 | 24 | var typeName: String { 25 | switch self { 26 | case .int: 27 | return "Int" 28 | case .double: 29 | return "Double" 30 | case .string: 31 | return "String" 32 | case .bool: 33 | return "Bool" 34 | case .data: 35 | return "Data" 36 | case let .array(content): 37 | return "[\(content.typeName)]" 38 | case let .dictionary(key, value): 39 | return "[\(key.typeName): \(value.typeName)]" 40 | case .unknown: 41 | return "Any" 42 | } 43 | } 44 | } 45 | 46 | func extractValueType(forKey key: String) -> ValueType? { 47 | guard let value = self.value(forKey: key) else { return nil } 48 | 49 | return self.extractType(for: value) 50 | } 51 | 52 | private func extractType(for value: Any) -> ValueType { 53 | if let _ = value as? Int { 54 | return .int 55 | } 56 | if let _ = value as? Double { 57 | return .double 58 | } 59 | if let _ = value as? String { 60 | return .string 61 | } 62 | if let array = value as? Array { 63 | var type: ValueType = .unknown 64 | if let data = array.first { 65 | type = extractType(for: data) 66 | } 67 | return .array(type) 68 | } 69 | if let dictionary = value as? Dictionary { 70 | var key: ValueType = .unknown 71 | var value: ValueType = .unknown 72 | if let data = dictionary.first { 73 | key = extractType(for: data.key) 74 | value = extractType(for: data.value) 75 | } 76 | 77 | return .dictionary(key, value) 78 | } 79 | 80 | return .unknown 81 | } 82 | } 83 | 84 | 85 | extension UserDefaults.ValueType { 86 | // FIXME: - support More Types 87 | func value(from stringValue: String) -> Any? { 88 | switch self { 89 | case .int: 90 | return Int(stringValue) 91 | case .double: 92 | return Double(stringValue) 93 | case .string: 94 | return stringValue 95 | default: 96 | return nil 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Example/Example/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UIApplicationSceneManifest 6 | 7 | UIApplicationSupportsMultipleScenes 8 | 9 | UISceneConfigurations 10 | 11 | UIWindowSceneSessionRoleApplication 12 | 13 | 14 | UISceneConfigurationName 15 | Default Configuration 16 | UISceneDelegateClassName 17 | $(PRODUCT_MODULE_NAME).SceneDelegate 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /Example/Example/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // Example 4 | // 5 | // Created by p-x9 on 2022/09/11. 6 | // 7 | // 8 | 9 | import UIKit 10 | 11 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 12 | 13 | var window: UIWindow? 14 | 15 | 16 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 17 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 18 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 19 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 20 | guard let windowScene = (scene as? UIWindowScene) else { return } 21 | setupWindow(with: windowScene) 22 | } 23 | 24 | func sceneDidDisconnect(_ scene: UIScene) { 25 | // Called as the scene is being released by the system. 26 | // This occurs shortly after the scene enters the background, or when its session is discarded. 27 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 28 | // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). 29 | } 30 | 31 | func sceneDidBecomeActive(_ scene: UIScene) { 32 | // Called when the scene has moved from an inactive state to an active state. 33 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 34 | } 35 | 36 | func sceneWillResignActive(_ scene: UIScene) { 37 | // Called when the scene will move from an active state to an inactive state. 38 | // This may occur due to temporary interruptions (ex. an incoming phone call). 39 | } 40 | 41 | func sceneWillEnterForeground(_ scene: UIScene) { 42 | // Called as the scene transitions from the background to the foreground. 43 | // Use this method to undo the changes made on entering the background. 44 | } 45 | 46 | func sceneDidEnterBackground(_ scene: UIScene) { 47 | // Called as the scene transitions from the foreground to the background. 48 | // Use this method to save data, release shared resources, and store enough scene-specific state information 49 | // to restore the scene back to its current state. 50 | } 51 | 52 | 53 | } 54 | 55 | extension SceneDelegate { 56 | func setupWindow(with windowScene: UIWindowScene) { 57 | let window = UIWindow(windowScene: windowScene) 58 | self.window = window 59 | 60 | let navigationController = UINavigationController(rootViewController: ViewController()) 61 | window.rootViewController = navigationController 62 | 63 | window.makeKeyAndVisible() 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Example/Example/TransitionPresenter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TransitionPresenter.swift 3 | // Example 4 | // 5 | // Created by p-x9 on 2022/10/03. 6 | // 7 | // 8 | 9 | import UIKit 10 | import AppContainer 11 | import AppContainerUI 12 | 13 | enum TransitionPresenter { 14 | static func pushAppContainerTableViewController(for appContainer: AppContainer) { 15 | let vc = AppContainerUI.ContainerListViewController(appContainer: appContainer) 16 | vc.title = "App Containers" 17 | UIApplication.shared.topViewController?.navigationController?.pushViewController(vc, animated: true) 18 | } 19 | 20 | static func pushContainerViewController(for container: Container, in appContainer: AppContainer? = nil) { 21 | let vc = AppContainerUI.ContainerInfoViewController(appContainer: appContainer, container: container) 22 | UIApplication.shared.topViewController?.navigationController?.pushViewController(vc, animated: true) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Example/Example/View/KeyValueTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyValueTableViewCell.swift 3 | // Example 4 | // 5 | // Created by p-x9 on 2022/09/11. 6 | // 7 | // 8 | 9 | import UIKit 10 | 11 | class KeyValueTableViewCell: UITableViewCell { 12 | 13 | let stackView: UIStackView = { 14 | let stackView = UIStackView() 15 | stackView.axis = .horizontal 16 | stackView.alignment = .center 17 | stackView.distribution = .fillEqually 18 | stackView.spacing = 8 19 | return stackView 20 | }() 21 | 22 | let keyLabel: UILabel = { 23 | let label = UILabel() 24 | label.numberOfLines = 0 25 | label.font = .systemFont(ofSize: 12) 26 | return label 27 | }() 28 | 29 | let valueLabel: UILabel = { 30 | let label = UILabel() 31 | label.numberOfLines = 0 32 | label.font = .systemFont(ofSize: 12) 33 | return label 34 | }() 35 | 36 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 37 | super.init(style: style, reuseIdentifier: reuseIdentifier) 38 | 39 | setupViews() 40 | setupViewConstraints() 41 | } 42 | 43 | required init?(coder: NSCoder) { 44 | fatalError("init(coder:) has not been implemented") 45 | } 46 | 47 | override func prepareForReuse() { 48 | super.prepareForReuse() 49 | 50 | keyLabel.text = "" 51 | valueLabel.text = "" 52 | } 53 | 54 | func setupViews() { 55 | contentView.addSubview(stackView) 56 | stackView.addArrangedSubview(keyLabel) 57 | stackView.addArrangedSubview(valueLabel) 58 | } 59 | 60 | func setupViewConstraints() { 61 | stackView.translatesAutoresizingMaskIntoConstraints = false 62 | 63 | NSLayoutConstraint.activate([ 64 | stackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 2), 65 | stackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -2), 66 | stackView.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 2), 67 | stackView.rightAnchor.constraint(equalTo: contentView.rightAnchor,constant: -2), 68 | ]) 69 | } 70 | 71 | func configure(key: String, value: Any?) { 72 | keyLabel.text = key 73 | if let value = value as? CustomStringConvertible { 74 | valueLabel.text = "\(value)" 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Example/Example/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Example 4 | // 5 | // Created by p-x9 on 2022/09/11. 6 | // 7 | // 8 | 9 | import UIKit 10 | import AppContainer 11 | import OrderedCollections 12 | 13 | class ViewController: UIViewController { 14 | 15 | let vi = View() 16 | 17 | let appContainer = AppContainer.standard 18 | let userDefaults = UserDefaults.standard 19 | //init(suiteName: "group.com.p-x9.AppContainerExample")! 20 | 21 | lazy var dictionary: Dictionary = .init() { 22 | didSet { 23 | var dictionary = OrderedDictionary(uniqueKeysWithValues: dictionary) 24 | dictionary.sort() 25 | orderedDictionary = dictionary 26 | } 27 | } 28 | 29 | var orderedDictionary: OrderedDictionary = .init() 30 | 31 | private let notificationCenter = NotificationCenter.default 32 | private var notificationObservations = [Any]() 33 | 34 | override func viewDidLoad() { 35 | super.viewDidLoad() 36 | 37 | setupViews() 38 | setupViewConstraints() 39 | setupNavigationItems() 40 | 41 | appContainer.delegates.add(self) 42 | } 43 | 44 | override func viewWillAppear(_ animated: Bool) { 45 | super.viewWillAppear(animated) 46 | 47 | title = appContainer.activeContainer?.name 48 | 49 | registerNotifications() 50 | } 51 | 52 | override func viewDidDisappear(_ animated: Bool) { 53 | super.viewDidDisappear(animated) 54 | 55 | unregisterNotifications() 56 | } 57 | 58 | private func setupViews() { 59 | view.addSubview(vi) 60 | 61 | vi.tableView.register(KeyValueTableViewCell.self, forCellReuseIdentifier: "\(KeyValueTableViewCell.self)") 62 | vi.tableView.dataSource = self 63 | vi.tableView.delegate = self 64 | 65 | refreshUserDefaultsTable() 66 | } 67 | 68 | private func setupViewConstraints() { 69 | vi.translatesAutoresizingMaskIntoConstraints = false 70 | 71 | NSLayoutConstraint.activate([ 72 | vi.topAnchor.constraint(equalTo: view.topAnchor), 73 | vi.bottomAnchor.constraint(equalTo: view.bottomAnchor), 74 | vi.leftAnchor.constraint(equalTo: view.leftAnchor), 75 | vi.rightAnchor.constraint(equalTo: view.rightAnchor), 76 | ]) 77 | } 78 | 79 | private func setupNavigationItems() { 80 | let addBarButtonItem = UIBarButtonItem(image: .init(systemName: "plus"), style: .plain, 81 | target: self, action: #selector(addItem)) 82 | 83 | let containerButtonItem = UIBarButtonItem(image: UIImage(systemName: "square.grid.3x3.square"), style: .plain, target: self, action: nil) 84 | 85 | navigationItem.leftBarButtonItem = containerButtonItem 86 | navigationItem.rightBarButtonItem = addBarButtonItem 87 | 88 | configureContainerMenu() 89 | } 90 | 91 | private func configureContainerMenu() { 92 | var actions = [UIMenuElement]() 93 | appContainer.containers.enumerated().forEach { i, container in 94 | let action = UIAction(title: container.name ?? "Container\(i)", 95 | subtitle: container.description, 96 | state: container == appContainer.activeContainer ? .on : .off) { [weak self] _ in 97 | guard let self = self, 98 | container != self.appContainer.activeContainer else { 99 | return 100 | } 101 | self.activate(container: container) 102 | self.configureContainerMenu() 103 | } 104 | actions.append(action) 105 | } 106 | let addContainerAction = UIAction(title: "Add New Container", image: UIImage(systemName: "plus")) { [weak self] _ in 107 | self?.addNewContainer() 108 | } 109 | 110 | let containerListAction = UIAction(title: "Detail...") { [weak self] _ in 111 | guard let self = self else { return } 112 | TransitionPresenter.pushAppContainerTableViewController(for: self.appContainer) 113 | } 114 | 115 | 116 | navigationItem.leftBarButtonItem?.menu = UIMenu(options: .displayInline, 117 | children: [ 118 | UIMenu(options: .displayInline, children: actions), 119 | UIMenu(options: .displayInline, children: [containerListAction]), 120 | addContainerAction 121 | ]) 122 | } 123 | 124 | private func activate(container: Container) { 125 | try? self.appContainer.activate(container: container) 126 | 127 | let alert = UIAlertController(title: "Restart App", 128 | message: "please restart app to activate selected container.", preferredStyle: .alert) 129 | let okAction = UIAlertAction(title: "OK", style: .default) { _ in 130 | UIControl().sendAction(NSSelectorFromString("suspend"), to: UIApplication.shared, for: nil) 131 | Timer.scheduledTimer(withTimeInterval: 0.3, repeats: false) { _ in 132 | exit(0) 133 | } 134 | } 135 | 136 | alert.addAction(okAction) 137 | 138 | present(alert, animated: true) 139 | } 140 | 141 | func refreshUserDefaultsTable() { 142 | self.dictionary = userDefaults.dictionaryRepresentation() 143 | self.vi.tableView.reloadData() 144 | } 145 | 146 | func addNewContainer() { 147 | let alert = UIAlertController(title: "Add New Container", message: nil, preferredStyle: .alert) 148 | alert.addTextField() 149 | alert.addTextField() 150 | 151 | guard let nameTextField = alert.textFields?[0], 152 | let descriptionTextField = alert.textFields?[1] else { 153 | return 154 | } 155 | 156 | nameTextField.placeholder = "Name" 157 | descriptionTextField.placeholder = "Description" 158 | 159 | let okAction = UIAlertAction(title: "Add", style: .destructive) { [weak self] _ in 160 | guard let self = self, let text = nameTextField.text, !text.isEmpty else { return } 161 | 162 | _ = try? self.appContainer.createNewContainer(name: text, description: descriptionTextField.text) 163 | self.configureContainerMenu() 164 | } 165 | 166 | let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil) 167 | 168 | alert.addAction(okAction) 169 | alert.addAction(cancelAction) 170 | 171 | present(alert, animated: true) 172 | } 173 | 174 | @objc 175 | func editItem(key: String) { 176 | guard let type = userDefaults.extractValueType(forKey: key), 177 | type.isEditable else { 178 | return 179 | } 180 | 181 | let alert = UIAlertController(title: "Edit", message: "key: \(key)", preferredStyle: .alert) 182 | alert.addTextField() 183 | 184 | guard let textField = alert.textFields?.first else { 185 | return 186 | } 187 | 188 | textField.placeholder = "Input \(String(describing: userDefaults.extractValueType(forKey: key)))" 189 | textField.text = "\(userDefaults.value(forKey: key) ?? "")" 190 | 191 | let okAction = UIAlertAction(title: "OK", style: .destructive) { [weak self] _ in 192 | guard let text = textField.text, !text.isEmpty else { return } 193 | 194 | guard let value: Any = type.value(from: text) else { return } 195 | 196 | self?.userDefaults.set(value, forKey: key) 197 | self?.refreshUserDefaultsTable() 198 | } 199 | 200 | let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil) 201 | 202 | alert.addAction(okAction) 203 | alert.addAction(cancelAction) 204 | 205 | present(alert, animated: true) 206 | } 207 | 208 | 209 | // Currently supported string only 210 | @objc 211 | func addItem() { 212 | let alert = UIAlertController(title: "Add", message: "Input key and value \n(string only)", preferredStyle: .alert) 213 | alert.addTextField() 214 | alert.addTextField() 215 | 216 | guard let keyTextField = alert.textFields?[0], 217 | let valueTextField = alert.textFields?[1] else { 218 | return 219 | } 220 | 221 | keyTextField.placeholder = "Key" 222 | valueTextField.placeholder = "Value" 223 | 224 | let okAction = UIAlertAction(title: "OK", style: .destructive) { [weak self] _ in 225 | guard let key = keyTextField.text else { return } 226 | self?.userDefaults.set(valueTextField.text, forKey: key) 227 | 228 | self?.refreshUserDefaultsTable() 229 | } 230 | 231 | let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil) 232 | 233 | alert.addAction(okAction) 234 | alert.addAction(cancelAction) 235 | 236 | present(alert, animated: true) 237 | } 238 | 239 | @objc 240 | func deleteItem(at index: Int) -> Bool { 241 | let item = self.orderedDictionary.elements[index] 242 | userDefaults.set(nil, forKey: item.key) 243 | let isDeleted = userDefaults.dictionaryRepresentation()[item.key] == nil 244 | if isDeleted { 245 | self.dictionary = userDefaults.dictionaryRepresentation() 246 | vi.tableView.deleteRows(at: [[0, index]], with: .automatic) 247 | } 248 | 249 | return isDeleted 250 | } 251 | 252 | } 253 | 254 | extension ViewController: UITableViewDataSource { 255 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 256 | dictionary.count 257 | } 258 | 259 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 260 | let cell = tableView.dequeueReusableCell(withIdentifier: "\(KeyValueTableViewCell.self)", 261 | for: indexPath) as! KeyValueTableViewCell 262 | 263 | let item = orderedDictionary.elements[indexPath.row] 264 | cell.configure(key: item.key, value: item.value) 265 | return cell 266 | } 267 | } 268 | 269 | extension ViewController: UITableViewDelegate { 270 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 271 | let key = orderedDictionary.keys[indexPath.item] 272 | self.editItem(key: key) 273 | } 274 | 275 | func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { 276 | let deleteAction = UIContextualAction(style: .destructive, title: "Delete") { [weak self] _, _, completionHandler in 277 | guard let self = self else { return } 278 | completionHandler(self.deleteItem(at: indexPath.item)) 279 | } 280 | return .init(actions: [deleteAction]) 281 | } 282 | } 283 | 284 | extension ViewController { 285 | func registerNotifications() { 286 | let willChangeObservation = notificationCenter.addObserver(forName: AppContainer.containerWillChangeNotification, object: nil, queue: .current) { _ in 287 | print("container will change") 288 | } 289 | 290 | let didChangeObservation = notificationCenter.addObserver(forName: AppContainer.containerDidChangeNotification, object: nil, queue: .current) { _ in 291 | print("container did change") 292 | } 293 | 294 | self.notificationObservations = [willChangeObservation, didChangeObservation] 295 | } 296 | 297 | func unregisterNotifications() { 298 | notificationObservations.forEach { 299 | notificationCenter.removeObserver($0) 300 | } 301 | } 302 | } 303 | 304 | extension ViewController: AppContainerDelegate { 305 | func appContainer(_ appContainer: AppContainer, willChangeTo toContainer: Container, from fromContainer: Container?) { 306 | print(#function, (fromContainer?.name ?? "") + " -> " + (toContainer.name ?? "")) 307 | } 308 | 309 | func appContainer(_ appContainer: AppContainer, didChangeTo toContainer: Container, from fromContainer: Container?) { 310 | print(#function, (fromContainer?.name ?? "") + " -> " + (toContainer.name ?? "")) 311 | } 312 | } 313 | 314 | extension ViewController { 315 | class View: UIView { 316 | let tableView = UITableView(frame: .null, style: .grouped) 317 | 318 | override init(frame: CGRect) { 319 | super.init(frame: frame) 320 | 321 | setupViews() 322 | setupViewConstraints() 323 | } 324 | 325 | required init?(coder: NSCoder) { 326 | fatalError("init(coder:) has not been implemented") 327 | } 328 | 329 | func setupViews() { 330 | addSubview(tableView) 331 | } 332 | 333 | func setupViewConstraints() { 334 | tableView.translatesAutoresizingMaskIntoConstraints = false 335 | 336 | NSLayoutConstraint.activate([ 337 | tableView.topAnchor.constraint(equalTo: topAnchor), 338 | tableView.bottomAnchor.constraint(equalTo: bottomAnchor), 339 | tableView.leftAnchor.constraint(equalTo: leftAnchor), 340 | tableView.rightAnchor.constraint(equalTo: rightAnchor), 341 | ]) 342 | } 343 | } 344 | } 345 | 346 | private extension UserDefaults.ValueType { 347 | // FIXME: - support more types 348 | var isEditable: Bool { 349 | switch self { 350 | case .int, .double, .string: 351 | return true 352 | default: 353 | return false 354 | } 355 | } 356 | } 357 | -------------------------------------------------------------------------------- /Example/ExampleTests/ExampleTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExampleTests.swift 3 | // Example 4 | // 5 | // Created by p-x9 on 2022/09/11. 6 | // 7 | // 8 | 9 | import XCTest 10 | @testable import Example 11 | 12 | class ExampleTests: XCTestCase { 13 | 14 | override func setUpWithError() throws { 15 | // Put setup code here. This method is called before the invocation of each test method in the class. 16 | } 17 | 18 | override func tearDownWithError() throws { 19 | // Put teardown code here. This method is called after the invocation of each test method in the class. 20 | } 21 | 22 | func testExample() throws { 23 | // This is an example of a functional test case. 24 | // Use XCTAssert and related functions to verify your tests produce the correct results. 25 | // Any test you write for XCTest can be annotated as throws and async. 26 | // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. 27 | // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. 28 | } 29 | 30 | func testPerformanceExample() throws { 31 | // This is an example of a performance test case. 32 | self.measure { 33 | // Put the code you want to measure the time of here. 34 | } 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /Example/ExampleUITests/ExampleUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExampleUITests.swift 3 | // Example 4 | // 5 | // Created by p-x9 on 2022/09/11. 6 | // 7 | // 8 | 9 | import XCTest 10 | 11 | class ExampleUITests: XCTestCase { 12 | 13 | override func setUpWithError() throws { 14 | // Put setup code here. This method is called before the invocation of each test method in the class. 15 | 16 | // In UI tests it is usually best to stop immediately when a failure occurs. 17 | continueAfterFailure = false 18 | 19 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 20 | } 21 | 22 | override func tearDownWithError() throws { 23 | // Put teardown code here. This method is called after the invocation of each test method in the class. 24 | } 25 | 26 | func testExample() throws { 27 | // UI tests must launch the application that they test. 28 | let app = XCUIApplication() 29 | app.launch() 30 | 31 | // Use XCTAssert and related functions to verify your tests produce the correct results. 32 | } 33 | 34 | func testLaunchPerformance() throws { 35 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { 36 | // This measures how long it takes to launch your application. 37 | measure(metrics: [XCTApplicationLaunchMetric()]) { 38 | XCUIApplication().launch() 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Example/ExampleUITests/ExampleUITestsLaunchTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExampleUITestsLaunchTests.swift 3 | // Example 4 | // 5 | // Created by p-x9 on 2022/09/11. 6 | // 7 | // 8 | 9 | import XCTest 10 | 11 | class ExampleUITestsLaunchTests: XCTestCase { 12 | 13 | override class var runsForEachTargetApplicationUIConfiguration: Bool { 14 | true 15 | } 16 | 17 | override func setUpWithError() throws { 18 | continueAfterFailure = false 19 | } 20 | 21 | func testLaunch() throws { 22 | let app = XCUIApplication() 23 | app.launch() 24 | 25 | // Insert steps here to perform after app launch but before taking a screenshot, 26 | // such as logging into a test account or navigating somewhere in the app 27 | 28 | let attachment = XCTAttachment(screenshot: app.screenshot()) 29 | attachment.name = "Launch Screen" 30 | attachment.lifetime = .keepAlways 31 | add(attachment) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Example/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.7 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "", 7 | platforms: [], 8 | products: [], 9 | dependencies: [], 10 | targets: [] 11 | ) 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 p-x9 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: docc 2 | docc: 3 | bash scripts/docc.sh 4 | 5 | .PHONY: docc-preview 6 | docc-preview: 7 | bash scripts/docc-preview.sh 8 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "editvalueview", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/p-x9/EditValueView.git", 7 | "state" : { 8 | "revision" : "4b28fb2e370f3e18a52e25a0fa5e50dd33fbb7fc", 9 | "version" : "0.4.0" 10 | } 11 | }, 12 | { 13 | "identity" : "keypathvalue", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/p-x9/KeyPathValue.git", 16 | "state" : { 17 | "revision" : "6aeb41d6c5564ae1f18e8b7a4bc5733d739558e3", 18 | "version" : "0.0.1" 19 | } 20 | }, 21 | { 22 | "identity" : "swiftuicolor", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/p-x9/SwiftUIColor.git", 25 | "state" : { 26 | "revision" : "61238f7460a04314dc059df68a1aa4c4b7dcb5df", 27 | "version" : "0.3.0" 28 | } 29 | } 30 | ], 31 | "version" : 2 32 | } 33 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "AppContainer", 7 | platforms: [ 8 | .iOS(.v14) 9 | ], 10 | products: [ 11 | .library( 12 | name: "AppContainer", 13 | targets: ["AppContainer"] 14 | ), 15 | .library( 16 | name: "AppContainerUI", 17 | targets: ["AppContainerUI"] 18 | ) 19 | ], 20 | dependencies: [ 21 | .package(url: "https://github.com/p-x9/EditValueView.git", .upToNextMinor(from: "0.4.0")), 22 | .package(url: "https://github.com/p-x9/KeyPathValue.git", .upToNextMinor(from: "0.0.1")) 23 | ], 24 | targets: [ 25 | .target( 26 | name: "AppContainer", 27 | dependencies: [ 28 | .product(name: "KeyPathValue", package: "KeyPathValue") 29 | ], 30 | swiftSettings: [ 31 | .enableUpcomingFeature("ExistentialAny", .when(configuration: .debug)) 32 | ], 33 | plugins: [] 34 | ), 35 | .target( 36 | name: "AppContainerUI", 37 | dependencies: [ 38 | "AppContainer", 39 | .product(name: "KeyPathValue", package: "KeyPathValue"), 40 | .product(name: "EditValueView", package: "EditValueView") 41 | ] 42 | ), 43 | .testTarget( 44 | name: "AppContainerTests", 45 | dependencies: ["AppContainer"] 46 | ) 47 | ] 48 | ) 49 | -------------------------------------------------------------------------------- /README.ja.md: -------------------------------------------------------------------------------- 1 | # AppContainer 2 | 3 | まるでコンテナを載せ替えるかのように、一つのアプリで複数の環境を作成・管理することのできるライブラリです。 4 | 5 | 6 | 7 | [![Github issues](https://img.shields.io/github/issues/p-x9/AppContainer)](https://github.com/p-x9/AppContainer/issues) 8 | [![Github forks](https://img.shields.io/github/forks/p-x9/AppContainer)](https://github.com/p-x9/AppContainer/network/members) 9 | [![Github stars](https://img.shields.io/github/stars/p-x9/AppContainer)](https://github.com/p-x9/AppContainer/stargazers) 10 | [![Github top language](https://img.shields.io/github/languages/top/p-x9/AppContainer)](https://github.com/p-x9/AppContainer/) 11 | 12 | ## コンセプト 13 | 通常1つのアプリに対して、1つの環境(ディレクトリ, UserDefaults、Cookie, Cache, …)が存在しています。 14 | Debugのためや複数のアカウントを扱うために複数の環境を用意するには、複数の同一アプリをインストールする必要があります。(bundle idの異なる) 15 | Debugにおいては、アカウントのログインとログアウトを繰り返しての確認が必要となるケースもあるかもしれません。 16 |
17 | そこで、同一アプリ内に複数の環境を作成し、簡単に切り替えることができないかと考えました。 18 | それで作成したのが、`AppContainer`というこのライブラリです。 19 | 20 | ## デモ 21 | | Default | Debug1 | 22 | | ---- | ---- | 23 | | ![Default](https://user-images.githubusercontent.com/50244599/195981131-c0a3938c-2ea9-48cc-a0f5-eafd7b6ea283.PNG) | ![Debug1](https://user-images.githubusercontent.com/50244599/195981134-bbd94cac-6cd2-4ea9-acbc-f20d3832fef6.PNG) | 24 | 25 | | コンテナ選択 | コンテナリスト | コンテナ情報 | 26 | | ---- | ---- | ---- | 27 | | ![Select](https://user-images.githubusercontent.com/50244599/195981135-240d3201-66e1-4845-b437-b8e28474a946.PNG) | ![List](https://user-images.githubusercontent.com/50244599/195981140-6ae77d07-6a7a-495a-812b-6bf2c4b81ce1.PNG) | ![Info](https://user-images.githubusercontent.com/50244599/195981142-21ac932a-d82e-41ce-a30d-deebd5773fdb.PNG) | 28 | 29 | ## 原理 30 | ### ディレクトリ 31 | アプリが書き込み可能な領域は、ホームディレクトリ配下にあります。 32 | UserDefaultsもCoreDataもCookieも、アプリが生成するデータは全てここに保存されています。 33 | このディレクトリをコンテナごとに載せ替えることで複数の環境を作成しています。 34 | コンテナは、Library配下に特別なディレクトリを用意してそこに退避させるように実装しています。 35 | ``` 36 | // UserDefaults 37 | Library/Preferences/XXXXX.plist 38 | 39 | // CoreData 40 | Library/Application Support/YOU_APP_NAME 41 | 42 | // Cookie 43 | Library/Cookies 44 | ``` 45 | 46 | ### UserDefaults/CFPreferences 47 | `UserDefaults`やその上位実装である`CFPreferences`はsetされたデータを、別プロセスである`cfprefsd`というものによってキャッシングをおこなっています。 48 | これらはsetされたデータをplistファイルに保存し永続化をおこなっていますが、上記のキャッシングにより、plist内のデータと`UserDefaults`/`CFPreferences`から取得できるデータは常に等しくなるわけではありません。(非同期で読み書きが行われる。) 49 | これはアプリの再起動を行っても同期されるとは限りません。 50 | よってコンテナの有効化処理を行う処理で、同期を行う処理をおこなっています。 51 | 52 | ### HTTPCookieStorage 53 | HTTPCookieStorageもキャッシングされており、非同期でファイル(Library/Cookies)への書き込みが行われています。 54 | 予期せぬタイミングで書き込みが行われてしまうと、コンテナ内でデータの不整合が起こってしまいます。 55 | 特に同一ドメイン宛のCookieを複数コンテナで扱っている場合には、セッションが引き継げなくなってしまう問題が起きます。 56 | そのため、コンテナの切り替え時に、保存とキャッシュの解放を行なっています。 57 | 58 | ## ドキュメント 59 | ### AppGroup 60 | ```swift 61 | extension AppContainer { 62 | static let group = .init(groupIdentifier: "YOUR APP GROUP IDENTIFIER") 63 | } 64 | ``` 65 | ### メソッド 66 | #### コンテナの作成 67 | ```swift 68 | let container = try AppContainer.standard.createNewContainer(name: "Debug1") 69 | ``` 70 | 71 | #### コンテナのリスト 72 | 元のコンテナは`DEFAULT`という名前で、UUIDは`00000000-0000-0000-0000-000000000000`となっています。 73 | `isDefault`というプロパティで確認できます。 74 | ```swift 75 | let containers: [Container] = AppContainer.standard.containers 76 | ``` 77 | 78 | #### 現在使用されているコンテナ 79 | ```swift 80 | let activeContainer: Container? = AppContainer.standard.activeContainer 81 | ``` 82 | 83 | #### コンテナの有効化 84 | このメソッドを呼んだ後は、アプリを再起動することをお勧めします。 85 | ```swift 86 | try AppContainer.standard.activate(container: container) 87 | ``` 88 | ```swift 89 | try AppContainer.standard.activateContainer(uuid: uuid) 90 | ``` 91 | 92 | #### コンテナの削除 93 | もし削除しようとしているコンテナが使用中の場合、Defaultコンテナを有効化してから削除します。 94 | ```swift 95 | try AppContainer.standard.delete(container: container) 96 | ``` 97 | ```swift 98 | try AppContainer.standard.deleteContainer(uuid: uuid) 99 | ``` 100 | 101 | #### コンテナの中身を初期化 102 | ```swift 103 | try AppContainer.standard.clean(container: container) 104 | ``` 105 | ```swift 106 | try AppContainer.standard.cleanContainer(uuid: uuid) 107 | ``` 108 | 109 | #### リセット 110 | このライブラリを使用する前の状態に戻します。 111 | 具体的には、DEFAULTコンテナを有効にして、その他のAppContainer関連のファイルは全て削除されます。 112 | ```swift 113 | try AppContainer.standard.reset() 114 | ``` 115 | 116 | ### 通知(Notification) 117 | コンテナ切り替え時に通知を受け取ることができます。 118 | 厳密に、切り替え前および切り替え後に行いたい処理を追加する場合は、後述するdelegateを使用してください。 119 | 120 | - containerWillChangeNotification 121 | コンテナ切り替え前 122 | - containerDidChangeNotification 123 | コンテナ切り替え後 124 | ### 委譲(Delegate) 125 | Delegateを使用して、コンテナの切り替え時に、任意の処理を追加することができます。 126 | 以下の順で処置が行われます。 127 | 128 | ``` swift 129 | // `activate`メソッドが呼び出される 130 | 131 | // ↓↓↓↓↓↓↓↓↓↓ 132 | 133 | 134 | func appContainer(_ appContainer: AppContainer, willChangeTo toContainer: Container, from fromContainer: Container?) // Delegate(コンテナ切り替え前) 135 | 136 | // ↓↓↓↓↓↓↓↓↓↓ 137 | 138 | // コンテナの切り替え処理(ライブラリ) 139 | 140 | // ↓↓↓↓↓↓↓↓↓↓ 141 | 142 | func appContainer(_ appContainer: AppContainer, didChangeTo toContainer: Container, from fromContainer: Container?) // Delegate(コンテナ切り替え後) 143 | ``` 144 | 145 | このライブラリでは複数のdelegateを設定できるようになっています。 146 | 以下のように追加します。 147 | ```swift 148 | AppContainer.standard.delegates.add(self) // selfがAppContainerDelegateに準拠している場合 149 | ``` 150 | 弱参照で保持されており、オブジェクトが解放された場合は自動で解除されます。 151 | もし、delegateの設定を解除したい場合は以下のように書きます。 152 | ```swift 153 | AppContainer.standard.delegates.remove(self) // selfがAppContainerDelegateに準拠している場合 154 | ``` 155 | 156 | ### コンテナ切り替え時に移動しないファイルを設定する 157 | コンテナ切り替え時には、一部のシステムファイルを除くほぼ全てのファイルが、コンテナディレクトリへ退避そして復元されます。 158 | これらの移動対象から除外するファイルを設定することができます。 159 | 160 | 例えば、以下はUserDefaultを全てのコンテナで共通で利用したいときの例です。 161 | このファイルは、コンテナ切り替え時に、退避も復元もされません。 162 | ```swift 163 | appcontainer.customExcludeFiles = [ 164 | "Library/Preferences/.plist" 165 | ] 166 | ``` 167 | 168 | ファイルパスのうち、最後がcustomExcludeFilesの内容に一致するものが全て移動対象から除外されます。 169 | 例えば、以下のように設定した場合、全てのディレクトリ配下の`XXX.yy`というファイルが移動対象から除外されます。 170 | ```swift 171 | appcontainer.customExcludeFiles = [ 172 | "XXX.yy" 173 | ] 174 | ``` 175 | 176 | ### AppContainerUI 177 | AppContainerを扱うためのUIを提供しています。 178 | SwiftUIおよびUIKitに対応しています。 179 | #### SwiftUI 180 | ```swift 181 | import AppContainerUI 182 | 183 | // コンテナのリストを表示 184 | ContainerListView(appContainer: .standard, title: String = "Containers") 185 | 186 | // コンテナ情報を表示 187 | ContainerInfoView(appContainer: .standard, container: container) 188 | ``` 189 | #### UIKit 190 | ```swift 191 | import AppContainerUI 192 | 193 | // コンテナのリストを表示 194 | ContainerListViewController(appContainer: .standard, title: String = "Containers") 195 | 196 | // コンテナ情報を表示 197 | ContainerInfoViewController(appContainer: .standard, container: container) 198 | ``` 199 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AppContainer 2 | 3 | This library allows you to create and manage multiple environments with a single application, just as if you were swapping containers. 4 | 5 | This is useful when you want to test multiple accounts in a staging application. 6 | 7 | 8 | 9 | [![Github issues](https://img.shields.io/github/issues/p-x9/AppContainer)](https://github.com/p-x9/AppContainer/issues) 10 | [![Github forks](https://img.shields.io/github/forks/p-x9/AppContainer)](https://github.com/p-x9/AppContainer/network/members) 11 | [![Github stars](https://img.shields.io/github/stars/p-x9/AppContainer)](https://github.com/p-x9/AppContainer/stargazers) 12 | [![Github top language](https://img.shields.io/github/languages/top/p-x9/AppContainer)](https://github.com/p-x9/AppContainer/) 13 | 14 | > Language Switch: [日本語](https://github.com/p-x9/AppContainer/blob/main/README.ja.md). 15 | 16 | ## Concept 17 | Normally there is one environment (Directory, UserDefaults, Cookies, Cache, ...) for one app. 18 | To have multiple environments for debugging or to handle multiple accounts, multiple identical apps must be installed. (with different bundle IDs). 19 | In debugging, there may be cases where accounts are repeatedly checked by logging in and logging out. 20 |
21 | Therefore, we thought it would be possible to create multiple environments within the same app and switch between them easily. 22 | This is why we created this library called `AppContainer`. 23 | 24 | ## Demo 25 | | Default | Debug1 | 26 | | ---- | ---- | 27 | | ![Default](https://user-images.githubusercontent.com/50244599/195981131-c0a3938c-2ea9-48cc-a0f5-eafd7b6ea283.PNG) | ![Debug1](https://user-images.githubusercontent.com/50244599/195981134-bbd94cac-6cd2-4ea9-acbc-f20d3832fef6.PNG) | 28 | 29 | | Selet Container | Container List | Container Info | 30 | | ---- | ---- | ---- | 31 | | ![Select](https://user-images.githubusercontent.com/50244599/195981135-240d3201-66e1-4845-b437-b8e28474a946.PNG) | ![List](https://user-images.githubusercontent.com/50244599/195981140-6ae77d07-6a7a-495a-812b-6bf2c4b81ce1.PNG) | ![Info](https://user-images.githubusercontent.com/50244599/195981142-21ac932a-d82e-41ce-a30d-deebd5773fdb.PNG) | 32 | 33 | ## Document 34 | ### AppGroup 35 | ```swift 36 | extension AppContainer { 37 | static let group = .init(groupIdentifier: "YOUR APP GROUP IDENTIFIER") 38 | } 39 | ``` 40 | ### Methods 41 | #### Create New Container 42 | ```swift 43 | let container = try AppContainer.standard.createNewContainer(name: "Debug1") 44 | ``` 45 | 46 | #### Get Container List 47 | The original container is named `DEFAULT` and has a UUID of `00000000-0000-0000-0000-0000-0000-00000000000000000000`. 48 | You can check it with the property `isDefault`. 49 | ```swift 50 | let containers: [Container] = AppContainer.standard.containers 51 | ``` 52 | 53 | #### Get Active Container 54 | ```swift 55 | let activeContainer: Container? = AppContainer.standard.activeContainer 56 | ``` 57 | 58 | #### Activate Contrainer 59 | It is recommended to restart the application after calling this method. 60 | ```swift 61 | try AppContainer.standard.activate(container: container) 62 | ``` 63 | ```swift 64 | try AppContainer.standard.activateContainer(uuid: uuid) 65 | ``` 66 | #### Delete Container 67 | If the container you are deleting is in use, activate the Default container before deleting it. 68 | ```swift 69 | try AppContainer.standard.delete(container: container) 70 | ``` 71 | ```swift 72 | try AppContainer.standard.deleteContainer(uuid: uuid) 73 | ``` 74 | 75 | #### Clean Container 76 | ```swift 77 | try AppContainer.standard.clean(container: container) 78 | ``` 79 | ```swift 80 | try AppContainer.standard.cleanContainer(uuid: uuid) 81 | ``` 82 | 83 | #### Reset 84 | Revert to the state before this library was used. 85 | Specifically, the DEFAULT container will be enabled and all other AppContainer-related files will be removed. 86 | ```swift 87 | try AppContainer.standard.reset() 88 | ``` 89 | 90 | ### Notification 91 | You can receive notifications when switching containers. 92 | If you want to add additional processing to be done strictly before and after the switch, use delegate as described below. 93 | 94 | - containerWillChangeNotification 95 | Before container switching 96 | - containerDidChangeNotification 97 | After container change 98 | 99 | ### Delegate 100 | Delegate can be used to add optional processing when switching containers. 101 | The actions are performed in the following order. 102 | 103 | ``` swift 104 | // the `activate` method is called 105 | 106 | // ↓↓↓↓↓↓↓↓↓↓ 107 | 108 | 109 | func appContainer(_ appContainer: AppContainer, willChangeTo toContainer: Container, from fromContainer: Container?) // Delegate(before container switch) 110 | 111 | // ↓↓↓↓↓↓↓↓↓↓ 112 | 113 | // Container switching process (library) 114 | 115 | // ↓↓↓↓↓↓↓↓↓↓ 116 | 117 | func appContainer(_ appContainer: AppContainer, didChangeTo toContainer: Container, from fromContainer: Container?) // Delegate (after container switch) 118 | ``` 119 | 120 | This library allows multiple delegates to be set. 121 | Add the following. 122 | 123 | ```swift 124 | AppContainer.standard.delegates.add(self) // if self is AppContainerDelegate compliant 125 | ``` 126 | It is held in a weak reference and will be automatically released when the object is freed. 127 | If you want to unset the delegate, write the following. 128 | ```swift 129 | AppContainer.standard.delegates.remove(self) // if self conforms to AppContainerDelegate 130 | ``` 131 | 132 | ### Set files not to be moved when switching containers 133 | When switching containers, almost all files except for some system files are saved and restored to the container directory. 134 | You can set files to be excluded from these moves. 135 | 136 | For example, the following is an example of a case where you want to use UserDefault commonly in all containers. 137 | This file will not be saved or restored when switching containers. 138 | ```swift 139 | appcontainer.customExcludeFiles = [ 140 | "Library/Preferences/.plist" 141 | ] 142 | ``` 143 | 144 | All file paths that end with the contents of customExcludeFiles will be excluded from the move. 145 | For example, the following configuration will exclude the file named `XXX.yy` under all directories. 146 | 147 | ```swift 148 | appcontainer.customExcludeFiles = [ 149 | "XXX.yy" 150 | ] 151 | ``` 152 | 153 | ### AppContainerUI 154 | Provides UI for using AppContainer. 155 | SwiftUI and UIKit are supported. 156 | #### SwiftUI 157 | ```swift 158 | import AppContainerUI 159 | 160 | // show Container List 161 | ContainerListView(appContainer: .standard, title: String = "Containers") 162 | 163 | // container info view 164 | ContainerInfoView(appContainer: .standard, container: container) 165 | ``` 166 | #### UIKit 167 | ```swift 168 | import AppContainerUI 169 | 170 | // show Container List 171 | ContainerListViewController(appContainer: .standard, title: String = "Containers") 172 | 173 | // container info view 174 | ContainerInfoViewController(appContainer: .standard, container: container) 175 | ``` 176 | 177 | ## Licenses 178 | 179 | [MIT License](./LICENSE) 180 | -------------------------------------------------------------------------------- /Sources/AppContainer/AppContainer.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import KeyPathValue 3 | 4 | /// Class for management containers 5 | /// 6 | /// It manages multiple containers and is responsible for activating and updating information. 7 | /// 8 | /// Usually, use the instance obtained with ``standard``. 9 | /// ```swift 10 | /// let appContainer = AppContainer.standard 11 | /// ``` 12 | /// When using appGroup, initialize it using ``init(groupIdentifier:)`` with its ID. 13 | /// ```swift 14 | /// let appContainer = AppContainer(groupIdentifier: "group.com.xxxx.yyyyy") 15 | /// ``` 16 | /// 17 | /// A list of currently existing containers can be obtained as follows 18 | /// ```swift 19 | /// let containers = appContainer.containers 20 | /// ``` 21 | /// 22 | /// #### Add New Container 23 | /// To add a container, write 24 | /// ```swift 25 | /// let newContainer = try appContainer.createNewContainer( 26 | /// name: "NAME", 27 | /// description: "DESCRIPTION" 28 | /// ) 29 | /// ``` 30 | /// 31 | /// #### Activate Container 32 | /// ```swift 33 | /// try appContainer.activate(container: newContainer) 34 | /// ``` 35 | /// 36 | public class AppContainer { 37 | /// Standard container manager 38 | public static let standard = AppContainer() 39 | 40 | /// Delegate list of ``AppContainer`` 41 | /// 42 | /// ``WeakHashTable`` is used to hold multiple objects by weak reference. 43 | /// Add a delegate as follows 44 | /// ```swift 45 | /// appContainer.delegates.add(self) 46 | /// ``` 47 | /// Remove a delegate as follows 48 | /// ```swift 49 | /// appContainer.delegates.remove(self) 50 | /// ``` 51 | public var delegates: WeakHashTable = .init() 52 | 53 | /// Files to exclude from movement when switching containers. 54 | /// 55 | /// For example, if you add "xxx.yy", all folders will exclude the following files named "xxx.yy". 56 | /// It is also possible to exclude only files named "XXX.yy" under a folder named “FFF" such as "FFF/XXX.yy". 57 | public var customExcludeFiles: [String] = [] 58 | 59 | private let fileManager = FileManager.default 60 | 61 | private let notificationCenter = NotificationCenter.default 62 | 63 | /// Home directory url 64 | private lazy var homeDirectoryUrl: URL = { 65 | URL(fileURLWithPath: NSHomeDirectory()) 66 | }() 67 | 68 | /// Home directory path 69 | private var homeDirectoryPath: String { 70 | homeDirectoryUrl.path 71 | } 72 | 73 | /// URL of app container stashed 74 | /// ~/Library/.__app_container__ 75 | private lazy var containersUrl: URL = { 76 | homeDirectoryUrl.appendingPathComponent("Library").appendingPathComponent(Constants.containerFolderName) 77 | }() 78 | 79 | /// App container settings plist path 80 | private lazy var settingsUrl: URL = { 81 | containersUrl.appendingPathComponent(Constants.appContainerSettingsPlistName) 82 | }() 83 | 84 | /// App container settings 85 | /// if update params, automatically update plist file 86 | private lazy var settings: AppContainerSettings = { 87 | loadAppContainerSettings() ?? .init(currentContainerUUID: UUID.zero.uuidString) 88 | }() { 89 | didSet { 90 | try? updateAppContainerSettings(settings: settings) 91 | } 92 | } 93 | 94 | private var groupIdentifier: String? 95 | 96 | /// Active Container. 97 | /// The original content now exists in the home directory. 98 | public var activeContainer: Container? { 99 | _containers.first(where: { $0.uuid == settings.currentContainerUUID }) 100 | } 101 | 102 | /// List of containers 103 | public var containers: [Container] { 104 | _containers 105 | } 106 | 107 | private lazy var _containers: [Container] = { 108 | (try? loadContainers()) ?? [] 109 | }() 110 | 111 | private var activeContainerIndex: Int? { 112 | _containers.firstIndex(where: { $0.uuid == settings.currentContainerUUID }) 113 | } 114 | 115 | /// suite names of UserDefaults. 116 | private var cachedSuiteNames = [String]() 117 | 118 | private init() { 119 | setup() 120 | } 121 | 122 | /// initialize with app group identifier. 123 | /// - Parameter groupIdentifier: app group identifier. 124 | public init(groupIdentifier: String) { 125 | guard let homeDirectoryUrl = fileManager.containerURL(forSecurityApplicationGroupIdentifier: groupIdentifier) else { 126 | fatalError("Invalid app group identifier") 127 | } 128 | 129 | self.homeDirectoryUrl = homeDirectoryUrl 130 | self.groupIdentifier = groupIdentifier 131 | setup() 132 | } 133 | 134 | private func setup() { 135 | try? createContainerDirectoryIfNeeded() 136 | try? createDefaultContainerIfNeeded() 137 | } 138 | 139 | /// Create new app container 140 | /// - Parameter name: container name 141 | /// - Returns: created container info 142 | @discardableResult 143 | public func createNewContainer(name: String, description: String? = nil) throws -> Container { 144 | try createNewContainer(name: name, 145 | description: description, 146 | isDefault: false) 147 | } 148 | 149 | /// Activate selected container. 150 | /// 151 | /// Since only the uuid of the container is considered, ``activateContainer(uuid:)``method can be used instead. 152 | /// 153 | /// - Parameter container: selected container. 154 | public func activate(container: Container) throws { 155 | if self.activeContainer?.uuid == container.uuid { 156 | return 157 | } 158 | 159 | let fromContainer = self.activeContainer 160 | 161 | notificationCenter.post(name: Self.containerWillChangeNotification, object: nil) 162 | delegates.objects.forEach { 163 | $0.appContainer(self, willChangeTo: container, from: fromContainer) 164 | } 165 | 166 | try exportUserDefaults() 167 | exportCookies() 168 | 169 | try stash() 170 | 171 | // clear `cfprefsd`'s cache 172 | try syncUserDefaults() 173 | 174 | try moveContainerContents(src: container.path(homeDirectoryPath), dst: homeDirectoryPath) 175 | 176 | try syncUserDefaults() 177 | 178 | settings.currentContainerUUID = container.uuid 179 | 180 | // increment activated count 181 | incrementActivatedCount(uuid: container.uuid) 182 | // update last activated date 183 | try? updateInfo(of: container, keyValue: .init(\.lastActivatedDate, Date())) 184 | 185 | notificationCenter.post(name: Self.containerDidChangeNotification, object: nil) 186 | delegates.objects.forEach { 187 | $0.appContainer(self, didChangeTo: container, from: fromContainer) 188 | } 189 | } 190 | 191 | /// Activate selected container 192 | /// - Parameter uuid: container's unique id. 193 | public func activateContainer(uuid: String) throws { 194 | guard let container = self.containers.first(where: { $0.uuid == uuid }) else { 195 | return 196 | } 197 | 198 | try self.activate(container: container) 199 | } 200 | 201 | /// Evacuate currently used container. 202 | public func stash() throws { 203 | guard let container = self.activeContainer else { 204 | return 205 | } 206 | 207 | try cleanContainerDirectory(container: container) 208 | try moveContainerContents(src: homeDirectoryPath, dst: container.path(homeDirectoryPath)) 209 | } 210 | 211 | /// Delete Selected container. 212 | /// If an attempt is made to delete a container currently in use, make the default container active 213 | /// 214 | /// Since only the uuid of the container is considered, ``deleteContainer(uuid:)``method can be used instead. 215 | /// 216 | /// - Parameter container: container that you want to delete. 217 | public func delete(container: Container) throws { 218 | guard let matchedIndex = _containers.firstIndex(where: { $0.uuid == container.uuid }), 219 | fileManager.fileExists(atPath: container.path(homeDirectoryPath)) else { 220 | throw AppContainerError.containerDirectoryNotFound 221 | } 222 | 223 | if settings.currentContainerUUID == container.uuid { 224 | try activate(container: .default) 225 | } 226 | 227 | try fileManager.removeItem(at: container.url(homeDirectoryUrl)) 228 | 229 | _containers.remove(at: matchedIndex) 230 | } 231 | 232 | /// Delete Selected container. 233 | /// - Parameter uuid: uuid of container that you want to delete. 234 | public func deleteContainer(uuid: String) throws { 235 | guard let container = self.containers.first(where: { $0.uuid == uuid }) else { 236 | return 237 | } 238 | 239 | try self.delete(container: container) 240 | } 241 | 242 | /// Clear contents in selected container 243 | /// 244 | /// Since only the uuid of the container is considered, ``cleanContainer(uuid:)``method can be used instead. 245 | /// 246 | /// - Parameter container: target container. 247 | public func clean(container: Container) throws { 248 | guard fileManager.fileExists(atPath: container.path(homeDirectoryPath)) else { 249 | throw AppContainerError.containerDirectoryNotFound 250 | } 251 | 252 | try Container.Directories.allCases.forEach { directory in 253 | let url = container.url(homeDirectoryUrl).appendingPathComponent(directory.name) 254 | try self.fileManager.removeChildContents(at: url, excludes: directory.excludes + customExcludeFiles) 255 | } 256 | } 257 | 258 | /// Clear contents in selected container 259 | /// - Parameter uuid: uuid of container that you want to clean. 260 | public func cleanContainer(uuid: String) throws { 261 | guard let container = self.containers.first(where: { $0.uuid == uuid }) else { 262 | return 263 | } 264 | 265 | try self.clean(container: container) 266 | } 267 | 268 | /// Clone selected container 269 | /// 270 | /// Since only the uuid of the container is considered, 271 | /// ``cloneContainer(uuid:with:description:)``method can be used instead. 272 | /// 273 | /// - Parameter container: target container. 274 | @discardableResult 275 | public func clone(container: Container, with name: String, description: String? = nil) throws -> Container { 276 | let newContainer = try createNewContainer(name: name, description: description) 277 | 278 | let src: String 279 | // if target container is in active, its contents is located in home. 280 | if activeContainer?.uuid == container.uuid { 281 | src = homeDirectoryPath 282 | } else { 283 | src = container.path(homeDirectoryPath) 284 | } 285 | 286 | try copyContainerContents(src: src, dst: newContainer.path(homeDirectoryPath)) 287 | 288 | return newContainer 289 | } 290 | 291 | /// Clone container 292 | /// - Parameter uuid: uuid of container that you want to clone. 293 | @discardableResult 294 | public func cloneContainer(uuid: String, with name: String, description: String? = nil) throws -> Container? { 295 | guard let container = self.containers.first(where: { $0.uuid == uuid }) else { 296 | return nil 297 | } 298 | 299 | return try clone(container: container, with: name, description: description) 300 | } 301 | 302 | /// Clear all containers and activate the default container 303 | public func reset() throws { 304 | try activate(container: .default) 305 | 306 | try fileManager.removeItem(at: containersUrl) 307 | } 308 | 309 | /// Update container informations 310 | /// 311 | /// Since only the uuid of the container is considered, ``updateContainerInfo(uuid:keyValue:)``method can be used instead. 312 | /// 313 | /// - Parameters: 314 | /// - container: target container. 315 | /// - keyValue: update key and value 316 | public func updateInfo(of container: Container, keyValue: WritableKeyPathWithValue) throws { 317 | try updateContainerInfo(uuid: container.uuid, keyValue: keyValue) 318 | } 319 | 320 | /// Update container informations 321 | /// - Parameters: 322 | /// - uuid: target container's uuid 323 | /// - keyValue: update key and value 324 | public func updateContainerInfo(uuid: String, keyValue: WritableKeyPathWithValue) throws { 325 | guard let matchedIndex = _containers.firstIndex(where: { $0.uuid == uuid }) else { 326 | return 327 | } 328 | 329 | keyValue.apply(&_containers[matchedIndex]) 330 | 331 | try saveContainerInfo(for: _containers[matchedIndex]) 332 | } 333 | } 334 | 335 | extension AppContainer { 336 | private func loadAppContainerSettings() -> AppContainerSettings? { 337 | guard let data = try? Data(contentsOf: settingsUrl) else { 338 | return nil 339 | } 340 | 341 | let decoder = PropertyListDecoder() 342 | return try? decoder.decode(AppContainerSettings.self, from: data) 343 | } 344 | 345 | private func updateAppContainerSettings(settings: AppContainerSettings) throws { 346 | if fileManager.fileExists(atPath: settingsUrl.path) { 347 | try fileManager.removeItem(at: settingsUrl) 348 | } 349 | 350 | // save plist 351 | let encoder = PropertyListEncoder() 352 | let containerData = try encoder.encode(settings) 353 | try containerData.write(to: settingsUrl) 354 | } 355 | } 356 | 357 | extension AppContainer { 358 | private func createContainerDirectoryIfNeeded() throws { 359 | try fileManager.createDirectory(at: containersUrl, withIntermediateDirectories: true) 360 | } 361 | 362 | private func createDefaultContainerIfNeeded() throws { 363 | guard !fileManager.fileExists(atPath: Container.default.path(homeDirectoryPath)) else { 364 | return 365 | } 366 | 367 | let container = try createNewContainer(name: "DEFAULT", 368 | description: nil, 369 | isDefault: true) 370 | 371 | try moveContainerContents(src: homeDirectoryPath, dst: container.path(homeDirectoryPath)) 372 | } 373 | 374 | @discardableResult 375 | private func createNewContainer(name: String, description: String?, isDefault: Bool) throws -> Container { 376 | let container: Container = isDefault ? .default : .init(name: name, uuid: UUID().uuidString, description: description) 377 | 378 | // create containers directory if needed 379 | try createContainerDirectoryIfNeeded() 380 | 381 | // create container directory 382 | try fileManager.createDirectoryIfNotExisted(at: container.url(homeDirectoryUrl), withIntermediateDirectories: true) 383 | 384 | try Container.Directories.allNames.forEach { name in 385 | let url = container.url(homeDirectoryUrl).appendingPathComponent(name) 386 | try self.fileManager.createDirectoryIfNotExisted(at: url, 387 | withIntermediateDirectories: true) 388 | } 389 | 390 | _containers.append(container) 391 | 392 | // create plist 393 | try saveContainerInfo(for: container) 394 | 395 | return container 396 | } 397 | 398 | /// Save container information. 399 | /// - Parameter container: target container 400 | private func saveContainerInfo(for container: Container) throws { 401 | guard fileManager.fileExists(atPath: container.path(homeDirectoryPath)) else { 402 | return 403 | } 404 | 405 | let plistUrl = container.url(homeDirectoryUrl).appendingPathComponent(Constants.containerInfoPlistName) 406 | 407 | if fileManager.fileExists(atPath: plistUrl.path) { 408 | try fileManager.removeItem(at: plistUrl) 409 | } 410 | 411 | // save plist 412 | let encoder = PropertyListEncoder() 413 | let containerData = try encoder.encode(container) 414 | try containerData.write(to: plistUrl) 415 | } 416 | 417 | /// load containers from app containers directory. 418 | /// container info is saved as property list in container directory's root. 419 | /// - Returns: App containers 420 | private func loadContainers() throws -> [Container] { 421 | guard fileManager.fileExists(atPath: containersUrl.path) else { 422 | return [] 423 | } 424 | 425 | let decoder = PropertyListDecoder() 426 | let uuids = try fileManager.contentsOfDirectory(atPath: containersUrl.path) 427 | let containers: [Container] = uuids.compactMap { uuid in 428 | let url = containersUrl.appendingPathComponent(uuid) 429 | let plistUrl = url.appendingPathComponent(Constants.containerInfoPlistName) 430 | guard let data = try? Data(contentsOf: plistUrl) else { 431 | return nil 432 | } 433 | return try? decoder.decode(Container.self, from: data) 434 | } 435 | 436 | return containers 437 | } 438 | 439 | /// Move container's child contents 440 | /// - Parameters: 441 | /// - src: source path. 442 | /// - dst: destination path. 443 | private func moveContainerContents(src: String, dst: String) throws { 444 | if src == dst { 445 | return 446 | } 447 | 448 | if groupIdentifier != nil { 449 | let excludes = Constants.appGroupExcludeFileNames + Container.Directories.allNames + customExcludeFiles 450 | try fileManager.createDirectoryIfNotExisted(atPath: dst, withIntermediateDirectories: true) 451 | try fileManager.removeChildContents(atPath: dst, excludes: excludes) 452 | try fileManager.moveChildContents(atPath: src, toPath: dst, excludes: excludes) 453 | } 454 | 455 | try Container.Directories.allCases.forEach { directory in 456 | let source = src + "/" + directory.name 457 | let destination = dst + "/" + directory.name 458 | 459 | let excludes = directory.excludes + customExcludeFiles 460 | 461 | try fileManager.createDirectoryIfNotExisted(atPath: destination, withIntermediateDirectories: true) 462 | try fileManager.removeChildContents(atPath: destination, excludes: excludes) 463 | try fileManager.moveChildContents(atPath: source, toPath: destination, excludes: excludes) 464 | } 465 | } 466 | 467 | /// Copy container's child contents 468 | /// - Parameters: 469 | /// - src: source path. 470 | /// - dst: destination path. 471 | private func copyContainerContents(src: String, dst: String) throws { 472 | if src == dst { 473 | return 474 | } 475 | 476 | if groupIdentifier != nil { 477 | let excludes = Constants.appGroupExcludeFileNames + Container.Directories.allNames + customExcludeFiles 478 | try fileManager.createDirectoryIfNotExisted(atPath: dst, withIntermediateDirectories: true) 479 | try fileManager.removeChildContents(atPath: dst, excludes: excludes) 480 | try fileManager.copyChildContents(atPath: src, toPath: dst, excludes: excludes) 481 | } 482 | 483 | try Container.Directories.allCases.forEach { directory in 484 | let source = src + "/" + directory.name 485 | let destination = dst + "/" + directory.name 486 | 487 | let excludes = directory.excludes + customExcludeFiles 488 | 489 | try fileManager.createDirectoryIfNotExisted(atPath: destination, withIntermediateDirectories: true) 490 | try fileManager.removeChildContents(atPath: destination, excludes: excludes) 491 | try fileManager.copyChildContents(atPath: source, toPath: destination, excludes: excludes) 492 | } 493 | } 494 | 495 | /// Delete container directory contents. 496 | /// - Parameter container: target container 497 | private func cleanContainerDirectory(container: Container) throws { 498 | try Container.Directories.allNames.forEach { name in 499 | let url = container.url(homeDirectoryUrl).appendingPathComponent(name) 500 | try self.fileManager.removeItemIfExisted(at: url) 501 | } 502 | } 503 | 504 | /// Increment container activated count 505 | /// - Parameter uuid: target container uuid 506 | private func incrementActivatedCount(uuid: String) { 507 | guard let matchedIndex = _containers.firstIndex(where: { $0.uuid == uuid }) else { 508 | return 509 | } 510 | let currentActivatedCount = _containers[matchedIndex].activatedCount ?? 0 511 | 512 | try? updateInfo(of: _containers[matchedIndex], 513 | keyValue: .init(\.activatedCount, currentActivatedCount + 1)) 514 | } 515 | } 516 | 517 | // MARK: - UserDefaults 518 | extension AppContainer { 519 | /// Reflect the contents of UserDefaults in the plist in the cache. 520 | private func syncUserDefaults() throws { 521 | let preferencesUrl = homeDirectoryUrl.appendingPathComponent("Library/Preferences") 522 | let suites = try fileManager.contentsOfDirectory(atPath: preferencesUrl.path) 523 | .filter { $0.hasSuffix(".plist") } 524 | .compactMap { $0.components(separatedBy: ".plist").first } 525 | .filter { !cachedSuiteNames.contains($0) } 526 | cachedSuiteNames += suites 527 | 528 | if let standard = groupIdentifier ?? Bundle.main.bundleIdentifier, 529 | !cachedSuiteNames.contains(standard) { 530 | cachedSuiteNames.append(standard) 531 | } 532 | 533 | cachedSuiteNames.forEach { 534 | syncUserDefaults(suiteName: $0) 535 | } 536 | } 537 | 538 | private func syncUserDefaults(suiteName: String?) { 539 | guard let plistName = suiteName ?? Bundle.main.bundleIdentifier else { return } 540 | let plistUrl = homeDirectoryUrl.appendingPathComponent("Library/Preferences/\(plistName).plist") 541 | 542 | let applicationID: CFString 543 | if let suiteName = suiteName, suiteName != Bundle.main.bundleIdentifier { 544 | applicationID = suiteName as CFString 545 | } else { 546 | applicationID = kCFPreferencesCurrentApplication 547 | } 548 | 549 | var udKeys = [CFString]() 550 | if let keys = CFPreferencesCopyKeyList(applicationID, kCFPreferencesCurrentUser, kCFPreferencesCurrentHost) { 551 | udKeys = [CFString](keys) 552 | } 553 | 554 | guard let plistDictionary = NSDictionary(contentsOf: plistUrl) as? [String: Any] else { 555 | udKeys.forEach { 556 | CFPreferencesSetAppValue($0, nil, applicationID) 557 | } 558 | return 559 | } 560 | 561 | udKeys.forEach { key in 562 | if let value = plistDictionary[key as String] { 563 | CFPreferencesSetAppValue(key, value as CFPropertyList, applicationID) 564 | } else { 565 | CFPreferencesSetAppValue(key, nil, applicationID) 566 | } 567 | } 568 | 569 | for (key, value) in plistDictionary { 570 | CFPreferencesSetAppValue(key as CFString, value as CFPropertyList, applicationID) 571 | } 572 | } 573 | 574 | /// Reflect the contents of the UserDefaults cache in the plist file. 575 | private func exportUserDefaults() throws { 576 | let preferencesUrl = homeDirectoryUrl.appendingPathComponent("Library/Preferences") 577 | var suites = try fileManager.contentsOfDirectory(atPath: preferencesUrl.path) 578 | .filter { $0.hasSuffix(".plist") } 579 | .compactMap { $0.components(separatedBy: ".plist").first } 580 | 581 | if let defaultSuite = groupIdentifier ?? Bundle.main.bundleIdentifier, 582 | !suites.contains(defaultSuite) { 583 | suites.append(defaultSuite) 584 | } 585 | 586 | cachedSuiteNames = suites 587 | 588 | try suites.forEach { 589 | try exportUserDefaults(suiteName: $0) 590 | } 591 | } 592 | 593 | private func exportUserDefaults(suiteName: String?) throws { 594 | guard let plistName = suiteName ?? Bundle.main.bundleIdentifier else { return } 595 | let plistUrl = homeDirectoryUrl.appendingPathComponent("Library/Preferences/\(plistName).plist") 596 | 597 | let applicationID: CFString 598 | if let suiteName = suiteName, suiteName != Bundle.main.bundleIdentifier { 599 | applicationID = suiteName as CFString 600 | } else { 601 | applicationID = kCFPreferencesCurrentApplication 602 | } 603 | 604 | CFPreferencesAppSynchronize(applicationID) 605 | 606 | guard let keys = CFPreferencesCopyKeyList(applicationID, kCFPreferencesCurrentUser, kCFPreferencesCurrentHost), 607 | let dictionary = CFPreferencesCopyMultiple(keys, applicationID, kCFPreferencesCurrentUser, kCFPreferencesCurrentHost) as? [String: Any] else { 608 | try fileManager.removeItemIfExisted(at: plistUrl) 609 | return 610 | } 611 | 612 | let plistData = try PropertyListSerialization.data(fromPropertyList: dictionary, format: .xml, options: 0) 613 | try plistData.write(to: plistUrl) 614 | } 615 | } 616 | 617 | extension AppContainer { 618 | /// Reflects the contents of HTTPCookie's cache in the file. 619 | private func exportCookies() { 620 | let cookieStorage: HTTPCookieStorage 621 | if let groupIdentifier = groupIdentifier { 622 | cookieStorage = HTTPCookieStorage.sharedCookieStorage(forGroupContainerIdentifier: groupIdentifier) 623 | } else { 624 | cookieStorage = .shared 625 | } 626 | cookieStorage.perform(NSSelectorFromString("_saveCookies")) 627 | } 628 | } 629 | -------------------------------------------------------------------------------- /Sources/AppContainer/AppContainerDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppContainerDelegate.swift 3 | // 4 | // 5 | // Created by p-x9 on 2023/03/06. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | public protocol AppContainerDelegate: AnyObject { 12 | /// Method called just before the container is switched. 13 | /// 14 | /// 15 | /// - Parameters: 16 | /// - appContainer: AppContainer 17 | /// - toContainer: Container to which to switch 18 | /// - fromContainer: Container from which to switch 19 | func appContainer(_ appContainer: AppContainer, willChangeTo toContainer: Container, from fromContainer: Container?) 20 | 21 | /// Method called just after the container is switched. 22 | /// 23 | /// 24 | /// - Parameters: 25 | /// - appContainer: AppContainer 26 | /// - toContainer: Container to which to switch 27 | /// - fromContainer: Container from which to switch 28 | func appContainer(_ appContainer: AppContainer, didChangeTo toContainer: Container, from fromContainer: Container?) 29 | } 30 | 31 | extension AppContainerDelegate { 32 | public func appContainer(_ appContainer: AppContainer, willChangeTo toContainer: Container, from fromContainer: Container?) {} 33 | public func appContainer(_ appContainer: AppContainer, didChangeTo toContainer: Container, from fromContainer: Container?) {} 34 | } 35 | -------------------------------------------------------------------------------- /Sources/AppContainer/AppContainerError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppContainerError.swift 3 | // 4 | // 5 | // Created by p-x9 on 2022/09/10. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | public enum AppContainerError: Error { 12 | case containerDirectoryNotFound 13 | } 14 | -------------------------------------------------------------------------------- /Sources/AppContainer/Constants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Constants.swift 3 | // 4 | // 5 | // Created by p-x9 on 2022/09/08. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | enum Constants { 12 | static let containerFolderName = ".__app_container__" 13 | 14 | static let appContainerSettingsPlistName = "com.p-x9.AppContainer.settings.plist" 15 | 16 | static let containerInfoPlistName = "container.plist" 17 | 18 | static let appGroupExcludeFileNames = [ 19 | ".com.apple.mobile_container_manager.metadata.plist", 20 | "container.plist" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /Sources/AppContainer/Extension/Array.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Array.swift 3 | // 4 | // 5 | // Created by p-x9 on 2022/09/24. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | extension Array { 12 | init(_ array: CFArray) { 13 | self = (0.. 0 { 155 | try self.removeItem(at: URL) 156 | } 157 | 158 | try contents.forEach { 159 | let URL = URL.appendingPathComponent($0) 160 | 161 | if !URL.shouldExclude(excludes: excludes) { 162 | if isDirectory(URL) { 163 | try self.removeChildContents(at: URL, excludes: excludes, level: level + 1) 164 | } else { 165 | try self.removeItem(at: URL) 166 | } 167 | } 168 | } 169 | } 170 | 171 | func removeChildContents(atPath path: String, excludes: [String] = []) throws { 172 | let URL = URL(fileURLWithPath: path) 173 | try self.removeChildContents(at: URL, excludes: excludes) 174 | } 175 | } 176 | 177 | extension FileManager { 178 | func isDirectory(_ path: String) -> Bool { 179 | var isDir: ObjCBool = false 180 | if fileExists(atPath: path, isDirectory: &isDir) { 181 | if isDir.boolValue { 182 | return true 183 | } 184 | } 185 | return false 186 | } 187 | 188 | func isDirectory(_ url: URL) -> Bool { 189 | var isDir: ObjCBool = false 190 | if fileExists(atPath: url.path, isDirectory: &isDir) { 191 | if isDir.boolValue { 192 | return true 193 | } 194 | } 195 | return false 196 | } 197 | } 198 | 199 | extension URL { 200 | func shouldExclude(excludes: [String]) -> Bool { 201 | for exclude in excludes { 202 | if path.hasSuffix(exclude) { 203 | return true 204 | } 205 | } 206 | return false 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /Sources/AppContainer/Extension/UUID.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UUID.swift 3 | // 4 | // 5 | // Created by p-x9 on 2022/09/08. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | extension UUID { 12 | // swiftlint:disable:next force_unwrapping 13 | static let zero: UUID = .init(uuidString: "00000000-0000-0000-0000-000000000000")! 14 | } 15 | -------------------------------------------------------------------------------- /Sources/AppContainer/Model/AppContainerSettings.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppContainerSettings.swift 3 | // 4 | // 5 | // Created by p-x9 on 2022/09/09. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | /// Setting information for AppContainer 12 | struct AppContainerSettings: Codable { 13 | /// UUID of the currently active container. 14 | var currentContainerUUID: String 15 | } 16 | -------------------------------------------------------------------------------- /Sources/AppContainer/Model/Container.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Container.swift 3 | // 4 | // 5 | // Created by p-x9 on 2022/09/08. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | /// Model of container 12 | /// 13 | /// Represents container information such as name, description, UUID, etc. 14 | public struct Container: Codable, Equatable { 15 | /// Container name 16 | public var name: String? 17 | /// Container unique id 18 | public let uuid: String 19 | 20 | /// Container description 21 | public var description: String? 22 | 23 | /// Container created date 24 | public let createdAt: Date? 25 | 26 | /// Last activated date 27 | public var lastActivatedDate: Date? 28 | 29 | /// Container activated count 30 | public var activatedCount: Int? = 0 31 | 32 | /// Default initializer 33 | /// - Parameters: 34 | /// - name: container name 35 | /// - uuid: container unique identifier 36 | /// - description: container description 37 | public init(name: String?, uuid: String, description: String? = nil) { 38 | self.name = name 39 | self.uuid = uuid 40 | self.description = description 41 | self.createdAt = Date() 42 | } 43 | } 44 | 45 | extension Container { 46 | /// A boolean value that indicates this container is default 47 | /// 48 | /// UUID of default container is `00000000-0000-0000-0000-000000000000` 49 | public var isDefault: Bool { 50 | uuid == UUID.zero.uuidString 51 | } 52 | 53 | /// Relative path where container is stored. 54 | /// Based on the app's home directory. 55 | private var relativePath: String { 56 | "Library/" + Constants.containerFolderName + "/" + uuid 57 | } 58 | 59 | /// Absolute URL where container is stored. 60 | /// - Parameter homeUrl: home directory url. 61 | public func url(_ homeUrl: URL) -> URL { 62 | homeUrl.appendingPathComponent(relativePath) 63 | } 64 | 65 | /// Absolute path where container is stored. 66 | /// - Parameter homePath: home directory path. 67 | public func path(_ homePath: String) -> String { 68 | homePath + "/" + relativePath 69 | } 70 | } 71 | 72 | extension Container { 73 | /// Default container 74 | /// 75 | /// The data of the app that existed before ``AppContainer`` is applied to this Default container. 76 | static let `default`: Container = { 77 | .init(name: "DEFAULT", uuid: UUID.zero.uuidString) 78 | }() 79 | } 80 | 81 | extension Container { 82 | enum Directories: String, CaseIterable { 83 | case library = "Library" 84 | case libraryCaches = "Library/Caches" 85 | case libraryPreferences = "Library/Preferences" 86 | case documents = "Documents" 87 | // case systemData = "SystemData" 88 | case tmp = "tmp" 89 | 90 | var name: String { 91 | rawValue 92 | } 93 | 94 | var excludes: [String] { 95 | switch self { 96 | case .library: 97 | return ["Caches", "Preferences"] 98 | default: 99 | return [] 100 | } 101 | } 102 | 103 | static var allNames: [String] { 104 | allCases.map(\.name) 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /Sources/AppContainer/Notification.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppContainer.Notification.swift 3 | // 4 | // 5 | // Created by p-x9 on 2023/02/27. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | extension AppContainer { 12 | public static let containerWillChangeNotification = Notification.Name("com.p-x9.appcontainer.containerWillChange") 13 | public static let containerDidChangeNotification = Notification.Name("com.p-x9.appcontainer.containerDidChange") 14 | } 15 | -------------------------------------------------------------------------------- /Sources/AppContainer/Util/WeakHashTable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WeakHashTable.swift 3 | // 4 | // 5 | // Created by p-x9 on 2023/03/10. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A class for holding multiple objects with weak references. 11 | /// 12 | /// Holds multiple objects by weak reference. 13 | /// Internally, NSHashTable is used. 14 | public class WeakHashTable { 15 | 16 | /// List of objects held with weak reference 17 | public var objects: [T] { 18 | accessQueue.sync { _objects.allObjects.compactMap { $0 as? T } } 19 | } 20 | 21 | private var _objects: NSHashTable = NSHashTable.weakObjects() 22 | private let accessQueue: DispatchQueue = DispatchQueue(label:"com.p-x9.appcintainer.WeakHashTable.\(T.self)", 23 | attributes: .concurrent) 24 | 25 | /// Default initializer 26 | public init() {} 27 | 28 | 29 | /// Initialize with initial value of object list 30 | /// - Parameter objects: initial value of object list 31 | public init(_ objects: [T]) { 32 | for object in objects { 33 | _objects.add(object as AnyObject) 34 | } 35 | } 36 | 37 | /// Add a object to be held with weak reference 38 | /// - Parameter object: Objects to be added 39 | public func add(_ object: T?) { 40 | accessQueue.sync(flags: .barrier) { 41 | _objects.add(object as AnyObject) 42 | } 43 | } 44 | 45 | /// Remove a object to be held with weak reference 46 | /// - Parameter object: Objects to be deleted. 47 | public func remove(_ object: T?) { 48 | accessQueue.sync(flags: .barrier) { 49 | _objects.remove(object as AnyObject) 50 | } 51 | } 52 | } 53 | 54 | 55 | extension WeakHashTable : Sequence { 56 | public typealias Iterator = Array.Iterator 57 | 58 | public func makeIterator() -> Iterator { 59 | return objects.makeIterator() 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Sources/AppContainerUI/ContainerInfoView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContainerInfoView.swift 3 | // 4 | // 5 | // Created by p-x9 on 2022/10/15. 6 | // 7 | // 8 | // swiftlint:disable:next type_contents_order 9 | 10 | import SwiftUI 11 | import AppContainer 12 | import EditValueView 13 | 14 | 15 | /// View to display container information 16 | @available(iOS 14, *) 17 | public struct ContainerInfoView: View { 18 | 19 | let appContainer: AppContainer? 20 | var isEditable: Bool { 21 | appContainer != nil 22 | } 23 | 24 | @State private var container: Container 25 | 26 | /// Default initializer 27 | /// 28 | /// The `appContainer` may be omitted, but if it is nil, each piece of information becomes uneditable. 29 | /// - Parameters: 30 | /// - appContainer: instance of ``AppContainer``. 31 | /// - container: target container 32 | public init(appContainer: AppContainer?, container: Container) { 33 | self.appContainer = appContainer 34 | self._container = .init(initialValue: container) 35 | } 36 | 37 | public var body: some View { 38 | List { 39 | informationSection 40 | if let activeContainer = appContainer?.activeContainer { 41 | Section { 42 | KeyValueRowView(key: "isActive", 43 | value: activeContainer.uuid == container.uuid) 44 | } 45 | } 46 | } 47 | .navigationTitle(container.name ?? "") 48 | .navigationBarTitleDisplayMode(.inline) 49 | } 50 | 51 | var informationSection: some View { 52 | Section(header: Text("Informations")) { 53 | WritableKeyValueRowView(key: "Name", value: container.name, isEditable: isEditable) { 54 | EditValueView(container, key: "name", keyPath: \.name) 55 | .onUpdate { value in 56 | save(keyPath: \.name, value: value) 57 | } 58 | } 59 | 60 | KeyValueRowView(key: "UUID", value: container.uuid) 61 | KeyValueRowView(key: "isDefault", value: container.isDefault) 62 | 63 | WritableKeyValueRowView(key: "Description", value: container.description, isEditable: isEditable) { 64 | EditValueView(container, key: "description", keyPath: \.description) 65 | .onUpdate { value in 66 | save(keyPath: \.description, value: value) 67 | } 68 | } 69 | 70 | KeyValueRowView(key: "Created At", value: container.createdAt) 71 | 72 | WritableKeyValueRowView(key: "Last Activated Date", value: container.lastActivatedDate, isEditable: isEditable) { 73 | EditValueView(container, key: "lastActivatedDate", keyPath: \.lastActivatedDate) 74 | .onUpdate { value in 75 | save(keyPath: \.lastActivatedDate, value: value) 76 | } 77 | } 78 | 79 | WritableKeyValueRowView(key: "Activated Count", value: container.activatedCount, isEditable: isEditable) { 80 | EditValueView(container, key: "activatedCount", keyPath: \.activatedCount) 81 | .onUpdate { value in 82 | save(keyPath: \.activatedCount, value: value) 83 | } 84 | } 85 | } 86 | } 87 | 88 | func save(keyPath: WritableKeyPath, value: Value) { 89 | try? appContainer?.updateInfo(of: container, 90 | keyValue: .init(keyPath, value)) 91 | onUpdate() 92 | } 93 | 94 | func onUpdate() { 95 | guard let appContainer = appContainer, 96 | let container = appContainer.containers.first(where: { container in 97 | self.container.uuid == container.uuid 98 | }) else { 99 | return 100 | } 101 | self.container = container 102 | } 103 | } 104 | 105 | #if DEBUG 106 | @available(iOS 14, *) 107 | struct ContainerInfoView_Preview: PreviewProvider { 108 | static var previews: some View { 109 | let container: Container = .init(name: "Default", 110 | uuid: UUID().uuidString, 111 | description: "This container is default.\nこんにちは") 112 | NavigationView { 113 | ContainerInfoView(appContainer: nil, container: container) 114 | } 115 | } 116 | } 117 | #endif 118 | -------------------------------------------------------------------------------- /Sources/AppContainerUI/ContainerListView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContainerListView.swift 3 | // 4 | // 5 | // Created by p-x9 on 2022/10/15. 6 | // 7 | // 8 | // swiftlint:disable:next type_contents_order 9 | 10 | import SwiftUI 11 | import AppContainer 12 | 13 | /// View to display list of existing containers 14 | @available(iOS 14, *) 15 | public struct ContainerListView: View { 16 | let appContainer: AppContainer 17 | let title: String 18 | 19 | @State var containers: [Container] 20 | 21 | /// Default initializer 22 | /// - Parameters: 23 | /// - appContainer: instance of ``AppContainer``. 24 | /// - title: navigation title 25 | public init(appContainer: AppContainer, title: String = "Containers") { 26 | self.appContainer = appContainer 27 | self.title = title 28 | self._containers = .init(initialValue: appContainer.containers) 29 | } 30 | 31 | public var body: some View { 32 | List { 33 | ForEach(containers) { container in 34 | NavigationLink { 35 | ContainerInfoView(appContainer: appContainer, 36 | container: container) 37 | } label: { 38 | let activeContainer = appContainer.activeContainer 39 | let isActive = activeContainer?.uuid == container.uuid 40 | ContainerRowView(container: container, isActive: isActive) 41 | } 42 | } 43 | } 44 | .onAppear { 45 | containers = appContainer.containers 46 | } 47 | .navigationTitle(title) 48 | .navigationBarTitleDisplayMode(.inline) 49 | } 50 | } 51 | 52 | extension Container: Identifiable { 53 | public var id: UUID { 54 | // swiftlint:disable:next force_unwrapping 55 | UUID(uuidString: uuid)! 56 | } 57 | } 58 | 59 | #if DEBUG 60 | @available(iOS 14, *) 61 | struct ContainerListView_Preview: PreviewProvider { 62 | static var previews: some View { 63 | let containers: [Container] = [ 64 | .init(name: "Default", 65 | uuid: UUID().uuidString, 66 | description: "This container is default.\nこんにちは"), 67 | .init(name: "Debug1", uuid: UUID().uuidString, 68 | description: "This container is Debug1. \nHello\nHello"), 69 | .init(name: "Debug2", uuid: UUID().uuidString), 70 | .init(name: "Debug3", uuid: UUID().uuidString) 71 | ] 72 | 73 | NavigationView { 74 | List { 75 | ForEach(containers) { container in 76 | NavigationLink { 77 | ContainerInfoView(appContainer: nil, container: container) 78 | } label: { 79 | ContainerRowView(container: container) 80 | } 81 | } 82 | } 83 | .navigationTitle("Containers") 84 | .navigationBarTitleDisplayMode(.inline) 85 | } 86 | } 87 | } 88 | #endif 89 | -------------------------------------------------------------------------------- /Sources/AppContainerUI/Extension/SwiftUI/View+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View+.swift 3 | // 4 | // 5 | // Created by p-x9 on 2022/10/18. 6 | // 7 | // 8 | 9 | import SwiftUI 10 | 11 | @available(iOS 13, *) 12 | extension View { 13 | @ViewBuilder 14 | func when(_ condition: Bool, @ViewBuilder transform: (Self) -> Content) -> some View { 15 | if condition { 16 | transform(self) 17 | } else { 18 | self 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/AppContainerUI/UIKit/ContainerInfoViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContainerInfoViewController.swift 3 | // 4 | // 5 | // Created by p-x9 on 2022/10/18. 6 | // 7 | // 8 | 9 | #if canImport(UIKit) 10 | import UIKit 11 | import SwiftUI 12 | import AppContainer 13 | 14 | /// View to display container information 15 | @available(iOS 14, *) 16 | public class ContainerInfoViewController: UIViewController { 17 | 18 | public let appContainer: AppContainer? 19 | private let container: Container // FIXME:uuid 20 | 21 | /// Default initializer 22 | /// 23 | /// The `appContainer` may be omitted, but if it is nil, each piece of information becomes uneditable. 24 | /// - Parameters: 25 | /// - appContainer: instance of ``AppContainer``. 26 | /// - container: target container 27 | public init(appContainer: AppContainer?, container: Container) { 28 | self.appContainer = appContainer 29 | self.container = container 30 | 31 | super.init(nibName: nil, bundle: nil) 32 | } 33 | 34 | @available(*, unavailable) 35 | required init?(coder: NSCoder) { 36 | fatalError("init(coder:) has not been implemented") 37 | } 38 | 39 | override public func viewDidLoad() { 40 | super.viewDidLoad() 41 | 42 | setupChildViewController() 43 | } 44 | 45 | private func setupChildViewController() { 46 | let containerInfoView = ContainerInfoView( 47 | appContainer: appContainer, 48 | container: container 49 | ) 50 | 51 | let vc = UIHostingController(rootView: containerInfoView) 52 | addChild(vc) 53 | view.addSubview(vc.view) 54 | vc.didMove(toParent: self) 55 | 56 | vc.view.translatesAutoresizingMaskIntoConstraints = false 57 | NSLayoutConstraint.activate([ 58 | vc.view.leftAnchor.constraint(equalTo: view.leftAnchor), 59 | vc.view.rightAnchor.constraint(equalTo: view.rightAnchor), 60 | vc.view.topAnchor.constraint(equalTo: view.topAnchor), 61 | vc.view.bottomAnchor.constraint(equalTo: view.bottomAnchor) 62 | ]) 63 | } 64 | } 65 | 66 | #endif 67 | -------------------------------------------------------------------------------- /Sources/AppContainerUI/UIKit/ContainerListViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContainerListViewController.swift 3 | // 4 | // 5 | // Created by p-x9 on 2022/10/18. 6 | // 7 | // 8 | 9 | #if canImport(UIKit) 10 | import UIKit 11 | import SwiftUI 12 | import AppContainer 13 | 14 | /// View to display list of existing containers 15 | @available(iOS 14, *) 16 | public class ContainerListViewController: UIViewController { 17 | 18 | /// Target appContainer 19 | public let appContainer: AppContainer 20 | 21 | /// Default initializer 22 | /// - Parameters: 23 | /// - appContainer: instance of ``AppContainer``. 24 | public init(appContainer: AppContainer) { 25 | self.appContainer = appContainer 26 | 27 | super.init(nibName: nil, bundle: nil) 28 | } 29 | 30 | @available(*, unavailable) 31 | required init?(coder: NSCoder) { 32 | fatalError("init(coder:) has not been implemented") 33 | } 34 | 35 | override public func viewDidLoad() { 36 | super.viewDidLoad() 37 | 38 | setupChildViewController() 39 | } 40 | 41 | private func setupChildViewController() { 42 | let containerListView = ContainerListView( 43 | appContainer: appContainer, 44 | title: title ?? "" 45 | ) 46 | 47 | let vc = UIHostingController(rootView: containerListView) 48 | addChild(vc) 49 | view.addSubview(vc.view) 50 | vc.didMove(toParent: self) 51 | 52 | vc.view.translatesAutoresizingMaskIntoConstraints = false 53 | NSLayoutConstraint.activate([ 54 | vc.view.leftAnchor.constraint(equalTo: view.leftAnchor), 55 | vc.view.rightAnchor.constraint(equalTo: view.rightAnchor), 56 | vc.view.topAnchor.constraint(equalTo: view.topAnchor), 57 | vc.view.bottomAnchor.constraint(equalTo: view.bottomAnchor) 58 | ]) 59 | } 60 | } 61 | 62 | #endif 63 | -------------------------------------------------------------------------------- /Sources/AppContainerUI/View/ContainerRowView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContainerRowView.swift 3 | // 4 | // 5 | // Created by p-x9 on 2022/10/15. 6 | // 7 | // 8 | 9 | import SwiftUI 10 | import AppContainer 11 | 12 | @available(iOS 13, *) 13 | struct ContainerRowView: View { 14 | 15 | private var container: Container 16 | private var isActive: Bool 17 | 18 | // swiftlint:disable:next type_contents_order 19 | init(container: Container, isActive: Bool = false) { 20 | self.container = container 21 | self.isActive = isActive 22 | } 23 | 24 | var body: some View { 25 | HStack(alignment: .center) { 26 | content 27 | if isActive { 28 | Color(UIColor.green) 29 | .frame(width: 8, height: 8) 30 | } 31 | } 32 | } 33 | 34 | var content: some View { 35 | VStack(alignment: .leading, spacing: 8) { 36 | HStack(alignment: .top) { 37 | Text(container.name ?? "") 38 | Text(container.description ?? "") 39 | .font(.caption) 40 | .foregroundColor(.secondary) 41 | Spacer() 42 | } 43 | 44 | HStack { 45 | Text(container.uuid) 46 | .font(.caption) 47 | .foregroundColor(.secondary) 48 | Spacer() 49 | } 50 | } 51 | } 52 | } 53 | 54 | #if DEBUG 55 | @available(iOS 13, *) 56 | struct ContainerRowView_Preview: PreviewProvider { 57 | static var previews: some View { 58 | let container: Container = .init(name: "Default", 59 | uuid: UUID().uuidString, 60 | description: "This container is default.\nこんにちは") 61 | Group { 62 | ContainerRowView(container: container) 63 | .previewLayout(.sizeThatFits) 64 | ContainerRowView(container: container, isActive: true) 65 | .previewLayout(.sizeThatFits) 66 | } 67 | } 68 | } 69 | #endif 70 | -------------------------------------------------------------------------------- /Sources/AppContainerUI/View/KeyValueRowView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyValueRowView.swift 3 | // 4 | // 5 | // Created by p-x9 on 2022/10/16. 6 | // 7 | // 8 | 9 | import SwiftUI 10 | 11 | @available(iOS 13, *) 12 | struct KeyValueRowView: View { 13 | let key: String 14 | let value: Any? 15 | var action: (() -> Void)? 16 | 17 | var body: some View { 18 | Button { 19 | action?() 20 | } label: { 21 | HStack(alignment: .center) { 22 | Text(key) 23 | .foregroundColor(.primary) 24 | Spacer() 25 | Text(stringValue()) 26 | .font(.caption) 27 | .foregroundColor(.secondary) 28 | } 29 | } 30 | 31 | } 32 | 33 | private func stringValue() -> String { 34 | var stringValue = String() 35 | if let value = value as? CustomStringConvertible { 36 | stringValue = value.description 37 | } 38 | return stringValue 39 | } 40 | } 41 | 42 | #if DEBUG 43 | @available(iOS 13, *) 44 | struct KeyValueRowView_Preview: PreviewProvider { 45 | static var previews: some View { 46 | KeyValueRowView(key: "Name", value: "Default") 47 | .previewLayout(.sizeThatFits) 48 | } 49 | } 50 | #endif 51 | -------------------------------------------------------------------------------- /Sources/AppContainerUI/View/WritableKeyValueRowView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WritableKeyValueRowView.swift 3 | // 4 | // 5 | // Created by p-x9 on 2022/10/24. 6 | // 7 | // 8 | 9 | import SwiftUI 10 | import EditValueView 11 | 12 | @available(iOS 13, *) 13 | struct WritableKeyValueRowView: View where Content: View { 14 | let key: String 15 | let value: Any? 16 | let isEditable: Bool 17 | var destination: Content? 18 | 19 | @State private var isPresentedSheet = false 20 | 21 | // swiftlint:disable:next type_contents_order 22 | init(key: String, value: Any?, isEditable: Bool, destination: (() -> Content)? = nil) { 23 | self.key = key 24 | self.value = value 25 | self.isEditable = isEditable 26 | self.destination = destination?() 27 | } 28 | 29 | var body: some View { 30 | Button { 31 | isPresentedSheet.toggle() 32 | } label: { 33 | HStack(alignment: .center) { 34 | Text(key) 35 | .foregroundColor(.primary) 36 | Spacer() 37 | Text(stringValue()) 38 | .font(.caption) 39 | .foregroundColor(.secondary) 40 | } 41 | } 42 | .when(destination != nil && isEditable) { 43 | $0.sheet(isPresented: $isPresentedSheet) { 44 | destination 45 | } 46 | } 47 | } 48 | 49 | private func stringValue() -> String { 50 | var stringValue = String() 51 | if let value = value as? CustomStringConvertible { 52 | stringValue = value.description 53 | } 54 | return stringValue 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Tests/AppContainerTests/AppContainerTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import AppContainer 3 | 4 | final class AppContainerTests: XCTestCase {} 5 | -------------------------------------------------------------------------------- /scripts/docc-preview.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | TARGET='AppContainer' 4 | 5 | preview_docc() { 6 | mkdir -p docs 7 | 8 | $(xcrun --find docc) preview \ 9 | "./${TARGET}.docc" \ 10 | --additional-symbol-graph-dir symbol-graphs \ 11 | --output-path "docs" 12 | } 13 | 14 | sh ./scripts/generate-symbols.sh 15 | 16 | preview_docc 17 | -------------------------------------------------------------------------------- /scripts/docc.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | TARGET='AppContainer' 4 | REPO_NAME='AppContainer' 5 | 6 | generate_docc() { 7 | mkdir -p docs 8 | 9 | $(xcrun --find docc) convert \ 10 | "./${TARGET}.docc" \ 11 | --output-path "docs" \ 12 | --hosting-base-path "${REPO_NAME}" \ 13 | --additional-symbol-graph-dir ./symbol-graphs 14 | } 15 | 16 | sh ./scripts/generate-symbols.sh 17 | 18 | generate_docc 19 | -------------------------------------------------------------------------------- /scripts/generate-symbols.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | SYMBOL_DIR='./symbol-graphs' 4 | 5 | clean_build() { 6 | rm -rf ./.build 7 | } 8 | 9 | clean_xcbuild() { 10 | destination=$1 11 | scheme=$2 12 | 13 | xcodebuild -scheme "$scheme" \ 14 | -destination "generic/platform=${destination}" \ 15 | clean 16 | } 17 | 18 | clean_symbol() { 19 | rm -rf SYMBOL_DIR 20 | } 21 | 22 | generate_symbol_graphs() { 23 | destination=$1 24 | scheme=$2 25 | 26 | mkdir -p .build/symbol-graphs 27 | mkdir -p symbol-graphs 28 | 29 | xcodebuild clean build -scheme "${scheme}"\ 30 | -destination "generic/platform=${destination}" \ 31 | OTHER_SWIFT_FLAGS="-emit-extension-block-symbols -emit-symbol-graph -emit-symbol-graph-dir $(pwd)/.build/symbol-graphs" 32 | 33 | mv "./.build/symbol-graphs/${scheme}.symbols.json" "${SYMBOL_DIR}/${scheme}_${destination}.symbols.json" 34 | } 35 | 36 | 37 | clean_build 38 | clean_xcbuild ios AppContainer 39 | clean_xcbuild ios AppContainerUI 40 | 41 | clean_symbol 42 | 43 | generate_symbol_graphs ios AppContainer 44 | generate_symbol_graphs ios AppContainerUI 45 | --------------------------------------------------------------------------------