├── .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 | 
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 | [](https://github.com/p-x9/AppContainer/issues)
8 | [](https://github.com/p-x9/AppContainer/network/members)
9 | [](https://github.com/p-x9/AppContainer/stargazers)
10 | [](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 | |  |  |
24 |
25 | | コンテナ選択 | コンテナリスト | コンテナ情報 |
26 | | ---- | ---- | ---- |
27 | |  |  |  |
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 | [](https://github.com/p-x9/AppContainer/issues)
10 | [](https://github.com/p-x9/AppContainer/network/members)
11 | [](https://github.com/p-x9/AppContainer/stargazers)
12 | [](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 | |  |  |
28 |
29 | | Selet Container | Container List | Container Info |
30 | | ---- | ---- | ---- |
31 | |  |  |  |
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 |
--------------------------------------------------------------------------------