├── .gitignore ├── .swiftlint.yml ├── .swiftpm └── xcode │ ├── package.xcworkspace │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ └── xcschemes │ └── EditValueView.xcscheme ├── EditValueView.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ ├── IDEWorkspaceChecks.plist │ └── swiftpm │ └── Package.resolved ├── Example ├── .swiftpm │ └── xcode │ │ └── package.xcworkspace │ │ └── contents.xcworkspacedata ├── Example.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved ├── Example │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── ContentView.swift │ ├── Example.entitlements │ ├── ExampleApp.swift │ ├── Info.plist │ └── Preview Content │ │ └── Preview Assets.xcassets │ │ └── Contents.json ├── ExampleTests │ └── ExampleTests.swift ├── ExampleUITests │ ├── ExampleUITests.swift │ └── ExampleUITestsLaunchTests.swift └── Package.swift ├── LICENSE ├── Package.resolved ├── Package.swift ├── README.md ├── Sources └── EditValueView │ ├── EditValueView+EditorType.swift │ ├── EditValueView+setValue.swift │ ├── EditValueView.swift │ ├── Editors │ ├── AnyJSONEditor.swift │ ├── CaseIterableEditor.swift │ ├── CodableEditorView.swift │ ├── ColorEditorView.swift │ ├── DateEditorView.swift │ └── Image │ │ ├── ImageEditor.swift │ │ ├── Picker │ │ ├── ImagePicker.swift │ │ └── SwiftUIImagePicker.swift │ │ └── SwiftUIImageEditor.swift │ ├── Extensions │ ├── CIImage+.swift │ ├── Codable+.swift │ ├── UIImage+.swift │ └── View+.swift │ ├── Protocol │ ├── DefaultRepresentable.swift │ ├── OptionalCaseIterable.swift │ ├── OptionalNumeric.swift │ └── OptionalType.swift │ ├── UIKit │ └── EditValueViewController.swift │ └── Util │ └── Typealias.swift └── Tests └── EditValueViewTests └── EditValueViewTests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm/config/registries.json 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .netrc 9 | 10 | ## Build generated 11 | build/ 12 | DerivedData/ 13 | 14 | ## Various settings 15 | *.pbxuser 16 | !default.pbxuser 17 | *.mode1v3 18 | !default.mode1v3 19 | *.mode2v3 20 | !default.mode2v3 21 | *.perspectivev3 22 | !default.perspectivev3 23 | xcuserdata/ 24 | node_modules/ 25 | 26 | ## Other 27 | *.moved-aside 28 | *.xccheckout 29 | *.xcscmblueprint 30 | *.DS_Store 31 | 32 | ## Obj-C/Swift specific 33 | *.hmap 34 | *.ipa 35 | *.dSYM.zip 36 | *.dSYM 37 | *.generated.swift 38 | 39 | 40 | Carthage/ 41 | Pods/ 42 | 43 | # CocoaPods 44 | # 45 | # We recommend against adding the Pods directory to your .gitignore. However 46 | # you should judge for yourself, the pros and cons are mentioned at: 47 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 48 | # 49 | 50 | # bundler 51 | vendor/bundle 52 | -------------------------------------------------------------------------------- /.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 | #- dynamic_inline 19 | #- empty_enum_arguments 20 | #- empty_parameters 21 | #- empty_parentheses_with_trailing_closure 22 | #- file_length 23 | #- for_where 24 | - force_cast 25 | #- force_try 26 | #- function_body_length 27 | #- function_parameter_count 28 | #- generic_type_name 29 | #- identifier_name 30 | #- implicit_getter 31 | #- inclusive_language 32 | #- inert_defer 33 | #- is_disjoint 34 | #- large_tuple 35 | #- leading_whitespace 36 | #- legacy_cggeometry_functions 37 | #- legacy_constant 38 | #- legacy_constructor 39 | #- legacy_hashing 40 | #- legacy_nsgeometry_functions 41 | #- line_length 42 | #- mark 43 | #- multiple_closures_with_trailing_closure 44 | #- nesting 45 | #- no_fallthrough_only 46 | #- no_space_in_method_call 47 | #- notification_center_detachment 48 | #- nsobject_prefer_isequal 49 | #- opening_brace 50 | #- operator_whitespace 51 | #- orphaned_doc_comment 52 | #- private_over_fileprivate 53 | #- private_unit_test 54 | #- protocol_property_accessors_order 55 | #- reduce_boolean 56 | #- redundant_discardable_let 57 | #- redundant_objc_attribute 58 | #- redundant_optional_initialization 59 | #- redundant_set_access_control 60 | #- redundant_string_enum_value 61 | #- redundant_void_return 62 | #- return_arrow_whitespace 63 | #- shorthand_operator 64 | #- statement_position 65 | #- superfluous_disable_command 66 | #- switch_case_alignment 67 | #- syntactic_sugar 68 | #- todo 69 | #- trailing_comma 70 | #- trailing_newline 71 | #- trailing_semicolon 72 | #- trailing_whitespace 73 | #- type_body_length 74 | #- type_name 75 | #- unneeded_break_in_switch 76 | #- unused_capture_list 77 | #- unused_closure_parameter 78 | #- unused_control_flow_label 79 | #- unused_enumerated 80 | #- unused_optional_binding 81 | #- unused_setter_value 82 | #- valid_ibinspectable 83 | #- vertical_parameter_alignment 84 | #- vertical_whitespace 85 | #- void_return 86 | #- weak_delegate 87 | #- xctfail_message 88 | 89 | 90 | opt_in_rules: 91 | - anyobject_protocol 92 | - array_init 93 | - attributes 94 | - balanced_xctest_lifecycle 95 | - capture_variable 96 | - closure_body_length 97 | - closure_end_indentation 98 | - closure_spacing 99 | - collection_alignment 100 | #- conditional_returns_on_newline 101 | - contains_over_filter_count 102 | - contains_over_filter_is_empty 103 | - contains_over_first_not_nil 104 | - contains_over_range_nil_comparison 105 | - convenience_type 106 | - discarded_notification_center_observer 107 | - discouraged_assert 108 | #- discouraged_object_literal 109 | - discouraged_optional_boolean 110 | - discouraged_optional_collection 111 | - empty_collection_literal 112 | - empty_count 113 | - empty_string 114 | - empty_xctest_method 115 | - enum_case_associated_values_count 116 | - expiring_todo 117 | #- explicit_acl 118 | #- explicit_enum_raw_value 119 | - explicit_init 120 | #- explicit_self 121 | #- explicit_top_level_acl 122 | #- explicit_type_interface 123 | #- extension_access_modifier 124 | - fallthrough 125 | - fatal_error_message 126 | #- file_header 127 | - file_name 128 | - file_name_no_space 129 | - file_types_order 130 | - first_where 131 | - flatmap_over_map_reduce 132 | - force_unwrapping 133 | - function_default_parameter_at_end 134 | - ibinspectable_in_extension 135 | - identical_operands 136 | - implicit_return 137 | #- implicitly_unwrapped_optional 138 | #- indentation_width 139 | - joined_default_parameter 140 | - last_where 141 | - legacy_multiple 142 | #- legacy_objc_type 143 | - legacy_random 144 | - let_var_whitespace 145 | - literal_expression_end_indentation 146 | - lower_acl_than_parent 147 | - missing_docs 148 | - modifier_order 149 | #- multiline_arguments 150 | #- multiline_arguments_brackets 151 | #- multiline_function_chains 152 | #- multiline_literal_brackets 153 | #- multiline_parameters 154 | #- multiline_parameters_brackets 155 | - nimble_operator 156 | #- no_extension_access_modifier 157 | #- no_grouping_extension 158 | - nslocalizedstring_key 159 | - nslocalizedstring_require_bundle 160 | #- number_separator 161 | #- object_literal 162 | - operator_usage_whitespace 163 | - optional_enum_case_matching 164 | - overridden_super_call 165 | - override_in_extension 166 | - pattern_matching_keywords 167 | - prefer_nimble 168 | - prefer_self_type_over_type_of_self 169 | - prefer_zero_over_explicit_init 170 | #- prefixed_toplevel_constant 171 | - private_action 172 | - private_outlet 173 | - private_subject 174 | #- prohibited_interface_builder 175 | - prohibited_super_call 176 | - quick_discouraged_call 177 | - quick_discouraged_focused_test 178 | - quick_discouraged_pending_test 179 | - raw_value_for_camel_cased_codable_enum 180 | - reduce_into 181 | - redundant_nil_coalescing 182 | - redundant_type_annotation 183 | #- required_deinit 184 | - required_enum_case 185 | - single_test_class 186 | - sorted_first_last 187 | #- sorted_imports 188 | - static_operator 189 | - strict_fileprivate 190 | #- strong_iboutlet 191 | - switch_case_on_newline 192 | - test_case_accessibility 193 | - toggle_bool 194 | - trailing_closure 195 | - type_contents_order 196 | - unavailable_function 197 | - unneeded_parentheses_in_closure_argument 198 | - unowned_variable_capture 199 | - untyped_error_in_catch 200 | - unused_declaration 201 | - unused_import 202 | - vertical_parameter_alignment_on_call 203 | #- vertical_whitespace_between_cases 204 | #- vertical_whitespace_closing_braces 205 | #- vertical_whitespace_opening_braces 206 | - xct_specific_matcher 207 | - yoda_condition 208 | 209 | included: 210 | 211 | excluded: 212 | - Pods 213 | - Carthage 214 | - SourcePackages 215 | - Generated 216 | - BuildTools 217 | 218 | line_length: 219 | warning: 300 220 | error: 500 221 | 222 | identifier_name: 223 | min_length: 224 | warning: 1 225 | excluded: 226 | - id 227 | - URL 228 | - GlobalAPIKey 229 | 230 | file_name: 231 | excluded: 232 | - main.swift 233 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/EditValueView.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 34 | 40 | 41 | 42 | 43 | 44 | 54 | 55 | 61 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /EditValueView.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /EditValueView.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /EditValueView.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "swift-magic-mirror", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/p-x9/swift-magic-mirror.git", 7 | "state" : { 8 | "revision" : "390e248dd6727e17aeb3949c12bb83e6eac876d1", 9 | "version" : "0.2.0" 10 | } 11 | }, 12 | { 13 | "identity" : "swiftui-reflection-view", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/p-x9/swiftui-reflection-view.git", 16 | "state" : { 17 | "revision" : "ce3b810ecfa1b2e308cbb5192ee4c8c43b1eefa8", 18 | "version" : "0.8.0" 19 | } 20 | }, 21 | { 22 | "identity" : "swiftuicolor", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/p-x9/SwiftUIColor.git", 25 | "state" : { 26 | "revision" : "aa42f452698cc0f78dcba2c6544f4abf7724602f", 27 | "version" : "0.4.0" 28 | } 29 | } 30 | ], 31 | "version" : 2 32 | } 33 | -------------------------------------------------------------------------------- /Example/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/Example.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 42B1D5292A6FFBBC00C06C53 /* ExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42B1D5282A6FFBBC00C06C53 /* ExampleApp.swift */; }; 11 | 42B1D52B2A6FFBBC00C06C53 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42B1D52A2A6FFBBC00C06C53 /* ContentView.swift */; }; 12 | 42B1D52D2A6FFBBE00C06C53 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 42B1D52C2A6FFBBE00C06C53 /* Assets.xcassets */; }; 13 | 42B1D5312A6FFBBE00C06C53 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 42B1D5302A6FFBBE00C06C53 /* Preview Assets.xcassets */; }; 14 | 42B1D53B2A6FFBBF00C06C53 /* ExampleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42B1D53A2A6FFBBF00C06C53 /* ExampleTests.swift */; }; 15 | 42B1D5452A6FFBBF00C06C53 /* ExampleUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42B1D5442A6FFBBF00C06C53 /* ExampleUITests.swift */; }; 16 | 42B1D5472A6FFBBF00C06C53 /* ExampleUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42B1D5462A6FFBBF00C06C53 /* ExampleUITestsLaunchTests.swift */; }; 17 | 42B1D5572A6FFD8C00C06C53 /* EditValueView in Frameworks */ = {isa = PBXBuildFile; productRef = 42B1D5562A6FFD8C00C06C53 /* EditValueView */; }; 18 | /* End PBXBuildFile section */ 19 | 20 | /* Begin PBXContainerItemProxy section */ 21 | 42B1D5372A6FFBBF00C06C53 /* PBXContainerItemProxy */ = { 22 | isa = PBXContainerItemProxy; 23 | containerPortal = 42B1D51D2A6FFBBC00C06C53 /* Project object */; 24 | proxyType = 1; 25 | remoteGlobalIDString = 42B1D5242A6FFBBC00C06C53; 26 | remoteInfo = Example; 27 | }; 28 | 42B1D5412A6FFBBF00C06C53 /* PBXContainerItemProxy */ = { 29 | isa = PBXContainerItemProxy; 30 | containerPortal = 42B1D51D2A6FFBBC00C06C53 /* Project object */; 31 | proxyType = 1; 32 | remoteGlobalIDString = 42B1D5242A6FFBBC00C06C53; 33 | remoteInfo = Example; 34 | }; 35 | /* End PBXContainerItemProxy section */ 36 | 37 | /* Begin PBXFileReference section */ 38 | 42B1D5252A6FFBBC00C06C53 /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; 39 | 42B1D5282A6FFBBC00C06C53 /* ExampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleApp.swift; sourceTree = ""; }; 40 | 42B1D52A2A6FFBBC00C06C53 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 41 | 42B1D52C2A6FFBBE00C06C53 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 42 | 42B1D52E2A6FFBBE00C06C53 /* Example.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Example.entitlements; sourceTree = ""; }; 43 | 42B1D5302A6FFBBE00C06C53 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 44 | 42B1D5362A6FFBBF00C06C53 /* ExampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ExampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 45 | 42B1D53A2A6FFBBF00C06C53 /* ExampleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleTests.swift; sourceTree = ""; }; 46 | 42B1D5402A6FFBBF00C06C53 /* ExampleUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ExampleUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 47 | 42B1D5442A6FFBBF00C06C53 /* ExampleUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleUITests.swift; sourceTree = ""; }; 48 | 42B1D5462A6FFBBF00C06C53 /* ExampleUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleUITestsLaunchTests.swift; sourceTree = ""; }; 49 | 42B1D5582A70DF9100C06C53 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 50 | 42B1D5592A743AC200C06C53 /* EditValueView */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = EditValueView; path = ..; sourceTree = ""; }; 51 | /* End PBXFileReference section */ 52 | 53 | /* Begin PBXFrameworksBuildPhase section */ 54 | 42B1D5222A6FFBBC00C06C53 /* Frameworks */ = { 55 | isa = PBXFrameworksBuildPhase; 56 | buildActionMask = 2147483647; 57 | files = ( 58 | 42B1D5572A6FFD8C00C06C53 /* EditValueView in Frameworks */, 59 | ); 60 | runOnlyForDeploymentPostprocessing = 0; 61 | }; 62 | 42B1D5332A6FFBBF00C06C53 /* Frameworks */ = { 63 | isa = PBXFrameworksBuildPhase; 64 | buildActionMask = 2147483647; 65 | files = ( 66 | ); 67 | runOnlyForDeploymentPostprocessing = 0; 68 | }; 69 | 42B1D53D2A6FFBBF00C06C53 /* Frameworks */ = { 70 | isa = PBXFrameworksBuildPhase; 71 | buildActionMask = 2147483647; 72 | files = ( 73 | ); 74 | runOnlyForDeploymentPostprocessing = 0; 75 | }; 76 | /* End PBXFrameworksBuildPhase section */ 77 | 78 | /* Begin PBXGroup section */ 79 | 42B1D51C2A6FFBBC00C06C53 = { 80 | isa = PBXGroup; 81 | children = ( 82 | 42B1D5532A6FFBF400C06C53 /* Packages */, 83 | 42B1D5272A6FFBBC00C06C53 /* Example */, 84 | 42B1D5392A6FFBBF00C06C53 /* ExampleTests */, 85 | 42B1D5432A6FFBBF00C06C53 /* ExampleUITests */, 86 | 42B1D5262A6FFBBC00C06C53 /* Products */, 87 | 42B1D5552A6FFD8C00C06C53 /* Frameworks */, 88 | ); 89 | sourceTree = ""; 90 | }; 91 | 42B1D5262A6FFBBC00C06C53 /* Products */ = { 92 | isa = PBXGroup; 93 | children = ( 94 | 42B1D5252A6FFBBC00C06C53 /* Example.app */, 95 | 42B1D5362A6FFBBF00C06C53 /* ExampleTests.xctest */, 96 | 42B1D5402A6FFBBF00C06C53 /* ExampleUITests.xctest */, 97 | ); 98 | name = Products; 99 | sourceTree = ""; 100 | }; 101 | 42B1D5272A6FFBBC00C06C53 /* Example */ = { 102 | isa = PBXGroup; 103 | children = ( 104 | 42B1D5582A70DF9100C06C53 /* Info.plist */, 105 | 42B1D5282A6FFBBC00C06C53 /* ExampleApp.swift */, 106 | 42B1D52A2A6FFBBC00C06C53 /* ContentView.swift */, 107 | 42B1D52C2A6FFBBE00C06C53 /* Assets.xcassets */, 108 | 42B1D52E2A6FFBBE00C06C53 /* Example.entitlements */, 109 | 42B1D52F2A6FFBBE00C06C53 /* Preview Content */, 110 | ); 111 | path = Example; 112 | sourceTree = ""; 113 | }; 114 | 42B1D52F2A6FFBBE00C06C53 /* Preview Content */ = { 115 | isa = PBXGroup; 116 | children = ( 117 | 42B1D5302A6FFBBE00C06C53 /* Preview Assets.xcassets */, 118 | ); 119 | path = "Preview Content"; 120 | sourceTree = ""; 121 | }; 122 | 42B1D5392A6FFBBF00C06C53 /* ExampleTests */ = { 123 | isa = PBXGroup; 124 | children = ( 125 | 42B1D53A2A6FFBBF00C06C53 /* ExampleTests.swift */, 126 | ); 127 | path = ExampleTests; 128 | sourceTree = ""; 129 | }; 130 | 42B1D5432A6FFBBF00C06C53 /* ExampleUITests */ = { 131 | isa = PBXGroup; 132 | children = ( 133 | 42B1D5442A6FFBBF00C06C53 /* ExampleUITests.swift */, 134 | 42B1D5462A6FFBBF00C06C53 /* ExampleUITestsLaunchTests.swift */, 135 | ); 136 | path = ExampleUITests; 137 | sourceTree = ""; 138 | }; 139 | 42B1D5532A6FFBF400C06C53 /* Packages */ = { 140 | isa = PBXGroup; 141 | children = ( 142 | 42B1D5592A743AC200C06C53 /* EditValueView */, 143 | ); 144 | name = Packages; 145 | sourceTree = ""; 146 | }; 147 | 42B1D5552A6FFD8C00C06C53 /* Frameworks */ = { 148 | isa = PBXGroup; 149 | children = ( 150 | ); 151 | name = Frameworks; 152 | sourceTree = ""; 153 | }; 154 | /* End PBXGroup section */ 155 | 156 | /* Begin PBXNativeTarget section */ 157 | 42B1D5242A6FFBBC00C06C53 /* Example */ = { 158 | isa = PBXNativeTarget; 159 | buildConfigurationList = 42B1D54A2A6FFBBF00C06C53 /* Build configuration list for PBXNativeTarget "Example" */; 160 | buildPhases = ( 161 | 42B1D5212A6FFBBC00C06C53 /* Sources */, 162 | 42B1D5222A6FFBBC00C06C53 /* Frameworks */, 163 | 42B1D5232A6FFBBC00C06C53 /* Resources */, 164 | ); 165 | buildRules = ( 166 | ); 167 | dependencies = ( 168 | ); 169 | name = Example; 170 | packageProductDependencies = ( 171 | 42B1D5562A6FFD8C00C06C53 /* EditValueView */, 172 | ); 173 | productName = Example; 174 | productReference = 42B1D5252A6FFBBC00C06C53 /* Example.app */; 175 | productType = "com.apple.product-type.application"; 176 | }; 177 | 42B1D5352A6FFBBF00C06C53 /* ExampleTests */ = { 178 | isa = PBXNativeTarget; 179 | buildConfigurationList = 42B1D54D2A6FFBBF00C06C53 /* Build configuration list for PBXNativeTarget "ExampleTests" */; 180 | buildPhases = ( 181 | 42B1D5322A6FFBBF00C06C53 /* Sources */, 182 | 42B1D5332A6FFBBF00C06C53 /* Frameworks */, 183 | 42B1D5342A6FFBBF00C06C53 /* Resources */, 184 | ); 185 | buildRules = ( 186 | ); 187 | dependencies = ( 188 | 42B1D5382A6FFBBF00C06C53 /* PBXTargetDependency */, 189 | ); 190 | name = ExampleTests; 191 | productName = ExampleTests; 192 | productReference = 42B1D5362A6FFBBF00C06C53 /* ExampleTests.xctest */; 193 | productType = "com.apple.product-type.bundle.unit-test"; 194 | }; 195 | 42B1D53F2A6FFBBF00C06C53 /* ExampleUITests */ = { 196 | isa = PBXNativeTarget; 197 | buildConfigurationList = 42B1D5502A6FFBBF00C06C53 /* Build configuration list for PBXNativeTarget "ExampleUITests" */; 198 | buildPhases = ( 199 | 42B1D53C2A6FFBBF00C06C53 /* Sources */, 200 | 42B1D53D2A6FFBBF00C06C53 /* Frameworks */, 201 | 42B1D53E2A6FFBBF00C06C53 /* Resources */, 202 | ); 203 | buildRules = ( 204 | ); 205 | dependencies = ( 206 | 42B1D5422A6FFBBF00C06C53 /* PBXTargetDependency */, 207 | ); 208 | name = ExampleUITests; 209 | productName = ExampleUITests; 210 | productReference = 42B1D5402A6FFBBF00C06C53 /* ExampleUITests.xctest */; 211 | productType = "com.apple.product-type.bundle.ui-testing"; 212 | }; 213 | /* End PBXNativeTarget section */ 214 | 215 | /* Begin PBXProject section */ 216 | 42B1D51D2A6FFBBC00C06C53 /* Project object */ = { 217 | isa = PBXProject; 218 | attributes = { 219 | BuildIndependentTargetsInParallel = 1; 220 | LastSwiftUpdateCheck = 1430; 221 | LastUpgradeCheck = 1430; 222 | TargetAttributes = { 223 | 42B1D5242A6FFBBC00C06C53 = { 224 | CreatedOnToolsVersion = 14.3.1; 225 | }; 226 | 42B1D5352A6FFBBF00C06C53 = { 227 | CreatedOnToolsVersion = 14.3.1; 228 | TestTargetID = 42B1D5242A6FFBBC00C06C53; 229 | }; 230 | 42B1D53F2A6FFBBF00C06C53 = { 231 | CreatedOnToolsVersion = 14.3.1; 232 | TestTargetID = 42B1D5242A6FFBBC00C06C53; 233 | }; 234 | }; 235 | }; 236 | buildConfigurationList = 42B1D5202A6FFBBC00C06C53 /* Build configuration list for PBXProject "Example" */; 237 | compatibilityVersion = "Xcode 14.0"; 238 | developmentRegion = en; 239 | hasScannedForEncodings = 0; 240 | knownRegions = ( 241 | en, 242 | Base, 243 | ); 244 | mainGroup = 42B1D51C2A6FFBBC00C06C53; 245 | productRefGroup = 42B1D5262A6FFBBC00C06C53 /* Products */; 246 | projectDirPath = ""; 247 | projectRoot = ""; 248 | targets = ( 249 | 42B1D5242A6FFBBC00C06C53 /* Example */, 250 | 42B1D5352A6FFBBF00C06C53 /* ExampleTests */, 251 | 42B1D53F2A6FFBBF00C06C53 /* ExampleUITests */, 252 | ); 253 | }; 254 | /* End PBXProject section */ 255 | 256 | /* Begin PBXResourcesBuildPhase section */ 257 | 42B1D5232A6FFBBC00C06C53 /* Resources */ = { 258 | isa = PBXResourcesBuildPhase; 259 | buildActionMask = 2147483647; 260 | files = ( 261 | 42B1D5312A6FFBBE00C06C53 /* Preview Assets.xcassets in Resources */, 262 | 42B1D52D2A6FFBBE00C06C53 /* Assets.xcassets in Resources */, 263 | ); 264 | runOnlyForDeploymentPostprocessing = 0; 265 | }; 266 | 42B1D5342A6FFBBF00C06C53 /* Resources */ = { 267 | isa = PBXResourcesBuildPhase; 268 | buildActionMask = 2147483647; 269 | files = ( 270 | ); 271 | runOnlyForDeploymentPostprocessing = 0; 272 | }; 273 | 42B1D53E2A6FFBBF00C06C53 /* Resources */ = { 274 | isa = PBXResourcesBuildPhase; 275 | buildActionMask = 2147483647; 276 | files = ( 277 | ); 278 | runOnlyForDeploymentPostprocessing = 0; 279 | }; 280 | /* End PBXResourcesBuildPhase section */ 281 | 282 | /* Begin PBXSourcesBuildPhase section */ 283 | 42B1D5212A6FFBBC00C06C53 /* Sources */ = { 284 | isa = PBXSourcesBuildPhase; 285 | buildActionMask = 2147483647; 286 | files = ( 287 | 42B1D52B2A6FFBBC00C06C53 /* ContentView.swift in Sources */, 288 | 42B1D5292A6FFBBC00C06C53 /* ExampleApp.swift in Sources */, 289 | ); 290 | runOnlyForDeploymentPostprocessing = 0; 291 | }; 292 | 42B1D5322A6FFBBF00C06C53 /* Sources */ = { 293 | isa = PBXSourcesBuildPhase; 294 | buildActionMask = 2147483647; 295 | files = ( 296 | 42B1D53B2A6FFBBF00C06C53 /* ExampleTests.swift in Sources */, 297 | ); 298 | runOnlyForDeploymentPostprocessing = 0; 299 | }; 300 | 42B1D53C2A6FFBBF00C06C53 /* Sources */ = { 301 | isa = PBXSourcesBuildPhase; 302 | buildActionMask = 2147483647; 303 | files = ( 304 | 42B1D5452A6FFBBF00C06C53 /* ExampleUITests.swift in Sources */, 305 | 42B1D5472A6FFBBF00C06C53 /* ExampleUITestsLaunchTests.swift in Sources */, 306 | ); 307 | runOnlyForDeploymentPostprocessing = 0; 308 | }; 309 | /* End PBXSourcesBuildPhase section */ 310 | 311 | /* Begin PBXTargetDependency section */ 312 | 42B1D5382A6FFBBF00C06C53 /* PBXTargetDependency */ = { 313 | isa = PBXTargetDependency; 314 | target = 42B1D5242A6FFBBC00C06C53 /* Example */; 315 | targetProxy = 42B1D5372A6FFBBF00C06C53 /* PBXContainerItemProxy */; 316 | }; 317 | 42B1D5422A6FFBBF00C06C53 /* PBXTargetDependency */ = { 318 | isa = PBXTargetDependency; 319 | target = 42B1D5242A6FFBBC00C06C53 /* Example */; 320 | targetProxy = 42B1D5412A6FFBBF00C06C53 /* PBXContainerItemProxy */; 321 | }; 322 | /* End PBXTargetDependency section */ 323 | 324 | /* Begin XCBuildConfiguration section */ 325 | 42B1D5482A6FFBBF00C06C53 /* Debug */ = { 326 | isa = XCBuildConfiguration; 327 | buildSettings = { 328 | ALWAYS_SEARCH_USER_PATHS = NO; 329 | CLANG_ANALYZER_NONNULL = YES; 330 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 331 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 332 | CLANG_ENABLE_MODULES = YES; 333 | CLANG_ENABLE_OBJC_ARC = YES; 334 | CLANG_ENABLE_OBJC_WEAK = YES; 335 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 336 | CLANG_WARN_BOOL_CONVERSION = YES; 337 | CLANG_WARN_COMMA = YES; 338 | CLANG_WARN_CONSTANT_CONVERSION = YES; 339 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 340 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 341 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 342 | CLANG_WARN_EMPTY_BODY = YES; 343 | CLANG_WARN_ENUM_CONVERSION = YES; 344 | CLANG_WARN_INFINITE_RECURSION = YES; 345 | CLANG_WARN_INT_CONVERSION = YES; 346 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 347 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 348 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 349 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 350 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 351 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 352 | CLANG_WARN_STRICT_PROTOTYPES = YES; 353 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 354 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 355 | CLANG_WARN_UNREACHABLE_CODE = YES; 356 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 357 | COPY_PHASE_STRIP = NO; 358 | DEBUG_INFORMATION_FORMAT = dwarf; 359 | ENABLE_STRICT_OBJC_MSGSEND = YES; 360 | ENABLE_TESTABILITY = YES; 361 | GCC_C_LANGUAGE_STANDARD = gnu11; 362 | GCC_DYNAMIC_NO_PIC = NO; 363 | GCC_NO_COMMON_BLOCKS = YES; 364 | GCC_OPTIMIZATION_LEVEL = 0; 365 | GCC_PREPROCESSOR_DEFINITIONS = ( 366 | "DEBUG=1", 367 | "$(inherited)", 368 | ); 369 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 370 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 371 | GCC_WARN_UNDECLARED_SELECTOR = YES; 372 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 373 | GCC_WARN_UNUSED_FUNCTION = YES; 374 | GCC_WARN_UNUSED_VARIABLE = YES; 375 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 376 | MTL_FAST_MATH = YES; 377 | ONLY_ACTIVE_ARCH = YES; 378 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 379 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 380 | }; 381 | name = Debug; 382 | }; 383 | 42B1D5492A6FFBBF00C06C53 /* Release */ = { 384 | isa = XCBuildConfiguration; 385 | buildSettings = { 386 | ALWAYS_SEARCH_USER_PATHS = NO; 387 | CLANG_ANALYZER_NONNULL = YES; 388 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 389 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 390 | CLANG_ENABLE_MODULES = YES; 391 | CLANG_ENABLE_OBJC_ARC = YES; 392 | CLANG_ENABLE_OBJC_WEAK = YES; 393 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 394 | CLANG_WARN_BOOL_CONVERSION = YES; 395 | CLANG_WARN_COMMA = YES; 396 | CLANG_WARN_CONSTANT_CONVERSION = YES; 397 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 398 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 399 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 400 | CLANG_WARN_EMPTY_BODY = YES; 401 | CLANG_WARN_ENUM_CONVERSION = YES; 402 | CLANG_WARN_INFINITE_RECURSION = YES; 403 | CLANG_WARN_INT_CONVERSION = YES; 404 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 405 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 406 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 407 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 408 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 409 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 410 | CLANG_WARN_STRICT_PROTOTYPES = YES; 411 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 412 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 413 | CLANG_WARN_UNREACHABLE_CODE = YES; 414 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 415 | COPY_PHASE_STRIP = NO; 416 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 417 | ENABLE_NS_ASSERTIONS = NO; 418 | ENABLE_STRICT_OBJC_MSGSEND = YES; 419 | GCC_C_LANGUAGE_STANDARD = gnu11; 420 | GCC_NO_COMMON_BLOCKS = YES; 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 | MTL_ENABLE_DEBUG_INFO = NO; 428 | MTL_FAST_MATH = YES; 429 | SWIFT_COMPILATION_MODE = wholemodule; 430 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 431 | }; 432 | name = Release; 433 | }; 434 | 42B1D54B2A6FFBBF00C06C53 /* Debug */ = { 435 | isa = XCBuildConfiguration; 436 | buildSettings = { 437 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 438 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 439 | CODE_SIGN_ENTITLEMENTS = Example/Example.entitlements; 440 | CODE_SIGN_STYLE = Automatic; 441 | CURRENT_PROJECT_VERSION = 1; 442 | DEVELOPMENT_ASSET_PATHS = "\"Example/Preview Content\""; 443 | DEVELOPMENT_TEAM = WLCQDVKTS9; 444 | ENABLE_HARDENED_RUNTIME = YES; 445 | ENABLE_PREVIEWS = YES; 446 | GENERATE_INFOPLIST_FILE = YES; 447 | INFOPLIST_FILE = Example/Info.plist; 448 | INFOPLIST_KEY_NSCameraUsageDescription = "To select image"; 449 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; 450 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; 451 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; 452 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; 453 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; 454 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; 455 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; 456 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; 457 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 458 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 459 | IPHONEOS_DEPLOYMENT_TARGET = 16.4; 460 | LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; 461 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; 462 | MACOSX_DEPLOYMENT_TARGET = 13.3; 463 | MARKETING_VERSION = 1.0; 464 | PRODUCT_BUNDLE_IDENTIFIER = "com.p-x9.Example"; 465 | PRODUCT_NAME = "$(TARGET_NAME)"; 466 | SDKROOT = auto; 467 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; 468 | SWIFT_EMIT_LOC_STRINGS = YES; 469 | SWIFT_VERSION = 5.0; 470 | TARGETED_DEVICE_FAMILY = "1,2"; 471 | }; 472 | name = Debug; 473 | }; 474 | 42B1D54C2A6FFBBF00C06C53 /* Release */ = { 475 | isa = XCBuildConfiguration; 476 | buildSettings = { 477 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 478 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 479 | CODE_SIGN_ENTITLEMENTS = Example/Example.entitlements; 480 | CODE_SIGN_STYLE = Automatic; 481 | CURRENT_PROJECT_VERSION = 1; 482 | DEVELOPMENT_ASSET_PATHS = "\"Example/Preview Content\""; 483 | DEVELOPMENT_TEAM = WLCQDVKTS9; 484 | ENABLE_HARDENED_RUNTIME = YES; 485 | ENABLE_PREVIEWS = YES; 486 | GENERATE_INFOPLIST_FILE = YES; 487 | INFOPLIST_FILE = Example/Info.plist; 488 | INFOPLIST_KEY_NSCameraUsageDescription = "To select image"; 489 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; 490 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; 491 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; 492 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; 493 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; 494 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; 495 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; 496 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; 497 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 498 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 499 | IPHONEOS_DEPLOYMENT_TARGET = 16.4; 500 | LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; 501 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; 502 | MACOSX_DEPLOYMENT_TARGET = 13.3; 503 | MARKETING_VERSION = 1.0; 504 | PRODUCT_BUNDLE_IDENTIFIER = "com.p-x9.Example"; 505 | PRODUCT_NAME = "$(TARGET_NAME)"; 506 | SDKROOT = auto; 507 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; 508 | SWIFT_EMIT_LOC_STRINGS = YES; 509 | SWIFT_VERSION = 5.0; 510 | TARGETED_DEVICE_FAMILY = "1,2"; 511 | }; 512 | name = Release; 513 | }; 514 | 42B1D54E2A6FFBBF00C06C53 /* Debug */ = { 515 | isa = XCBuildConfiguration; 516 | buildSettings = { 517 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 518 | BUNDLE_LOADER = "$(TEST_HOST)"; 519 | CODE_SIGN_STYLE = Automatic; 520 | CURRENT_PROJECT_VERSION = 1; 521 | DEVELOPMENT_TEAM = WLCQDVKTS9; 522 | GENERATE_INFOPLIST_FILE = YES; 523 | IPHONEOS_DEPLOYMENT_TARGET = 16.4; 524 | MACOSX_DEPLOYMENT_TARGET = 13.3; 525 | MARKETING_VERSION = 1.0; 526 | PRODUCT_BUNDLE_IDENTIFIER = "com.p-x9.ExampleTests"; 527 | PRODUCT_NAME = "$(TARGET_NAME)"; 528 | SDKROOT = auto; 529 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; 530 | SWIFT_EMIT_LOC_STRINGS = NO; 531 | SWIFT_VERSION = 5.0; 532 | TARGETED_DEVICE_FAMILY = "1,2"; 533 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Example"; 534 | }; 535 | name = Debug; 536 | }; 537 | 42B1D54F2A6FFBBF00C06C53 /* Release */ = { 538 | isa = XCBuildConfiguration; 539 | buildSettings = { 540 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 541 | BUNDLE_LOADER = "$(TEST_HOST)"; 542 | CODE_SIGN_STYLE = Automatic; 543 | CURRENT_PROJECT_VERSION = 1; 544 | DEVELOPMENT_TEAM = WLCQDVKTS9; 545 | GENERATE_INFOPLIST_FILE = YES; 546 | IPHONEOS_DEPLOYMENT_TARGET = 16.4; 547 | MACOSX_DEPLOYMENT_TARGET = 13.3; 548 | MARKETING_VERSION = 1.0; 549 | PRODUCT_BUNDLE_IDENTIFIER = "com.p-x9.ExampleTests"; 550 | PRODUCT_NAME = "$(TARGET_NAME)"; 551 | SDKROOT = auto; 552 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; 553 | SWIFT_EMIT_LOC_STRINGS = NO; 554 | SWIFT_VERSION = 5.0; 555 | TARGETED_DEVICE_FAMILY = "1,2"; 556 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Example"; 557 | }; 558 | name = Release; 559 | }; 560 | 42B1D5512A6FFBBF00C06C53 /* Debug */ = { 561 | isa = XCBuildConfiguration; 562 | buildSettings = { 563 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 564 | CODE_SIGN_STYLE = Automatic; 565 | CURRENT_PROJECT_VERSION = 1; 566 | DEVELOPMENT_TEAM = WLCQDVKTS9; 567 | GENERATE_INFOPLIST_FILE = YES; 568 | IPHONEOS_DEPLOYMENT_TARGET = 16.4; 569 | MACOSX_DEPLOYMENT_TARGET = 13.3; 570 | MARKETING_VERSION = 1.0; 571 | PRODUCT_BUNDLE_IDENTIFIER = "com.p-x9.ExampleUITests"; 572 | PRODUCT_NAME = "$(TARGET_NAME)"; 573 | SDKROOT = auto; 574 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; 575 | SWIFT_EMIT_LOC_STRINGS = NO; 576 | SWIFT_VERSION = 5.0; 577 | TARGETED_DEVICE_FAMILY = "1,2"; 578 | TEST_TARGET_NAME = Example; 579 | }; 580 | name = Debug; 581 | }; 582 | 42B1D5522A6FFBBF00C06C53 /* Release */ = { 583 | isa = XCBuildConfiguration; 584 | buildSettings = { 585 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 586 | CODE_SIGN_STYLE = Automatic; 587 | CURRENT_PROJECT_VERSION = 1; 588 | DEVELOPMENT_TEAM = WLCQDVKTS9; 589 | GENERATE_INFOPLIST_FILE = YES; 590 | IPHONEOS_DEPLOYMENT_TARGET = 16.4; 591 | MACOSX_DEPLOYMENT_TARGET = 13.3; 592 | MARKETING_VERSION = 1.0; 593 | PRODUCT_BUNDLE_IDENTIFIER = "com.p-x9.ExampleUITests"; 594 | PRODUCT_NAME = "$(TARGET_NAME)"; 595 | SDKROOT = auto; 596 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; 597 | SWIFT_EMIT_LOC_STRINGS = NO; 598 | SWIFT_VERSION = 5.0; 599 | TARGETED_DEVICE_FAMILY = "1,2"; 600 | TEST_TARGET_NAME = Example; 601 | }; 602 | name = Release; 603 | }; 604 | /* End XCBuildConfiguration section */ 605 | 606 | /* Begin XCConfigurationList section */ 607 | 42B1D5202A6FFBBC00C06C53 /* Build configuration list for PBXProject "Example" */ = { 608 | isa = XCConfigurationList; 609 | buildConfigurations = ( 610 | 42B1D5482A6FFBBF00C06C53 /* Debug */, 611 | 42B1D5492A6FFBBF00C06C53 /* Release */, 612 | ); 613 | defaultConfigurationIsVisible = 0; 614 | defaultConfigurationName = Release; 615 | }; 616 | 42B1D54A2A6FFBBF00C06C53 /* Build configuration list for PBXNativeTarget "Example" */ = { 617 | isa = XCConfigurationList; 618 | buildConfigurations = ( 619 | 42B1D54B2A6FFBBF00C06C53 /* Debug */, 620 | 42B1D54C2A6FFBBF00C06C53 /* Release */, 621 | ); 622 | defaultConfigurationIsVisible = 0; 623 | defaultConfigurationName = Release; 624 | }; 625 | 42B1D54D2A6FFBBF00C06C53 /* Build configuration list for PBXNativeTarget "ExampleTests" */ = { 626 | isa = XCConfigurationList; 627 | buildConfigurations = ( 628 | 42B1D54E2A6FFBBF00C06C53 /* Debug */, 629 | 42B1D54F2A6FFBBF00C06C53 /* Release */, 630 | ); 631 | defaultConfigurationIsVisible = 0; 632 | defaultConfigurationName = Release; 633 | }; 634 | 42B1D5502A6FFBBF00C06C53 /* Build configuration list for PBXNativeTarget "ExampleUITests" */ = { 635 | isa = XCConfigurationList; 636 | buildConfigurations = ( 637 | 42B1D5512A6FFBBF00C06C53 /* Debug */, 638 | 42B1D5522A6FFBBF00C06C53 /* Release */, 639 | ); 640 | defaultConfigurationIsVisible = 0; 641 | defaultConfigurationName = Release; 642 | }; 643 | /* End XCConfigurationList section */ 644 | 645 | /* Begin XCSwiftPackageProductDependency section */ 646 | 42B1D5562A6FFD8C00C06C53 /* EditValueView */ = { 647 | isa = XCSwiftPackageProductDependency; 648 | productName = EditValueView; 649 | }; 650 | /* End XCSwiftPackageProductDependency section */ 651 | }; 652 | rootObject = 42B1D51D2A6FFBBC00C06C53 /* Project object */; 653 | } 654 | -------------------------------------------------------------------------------- /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.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "swiftuicolor", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/p-x9/SwiftUIColor.git", 7 | "state" : { 8 | "revision" : "61238f7460a04314dc059df68a1aa4c4b7dcb5df", 9 | "version" : "0.3.0" 10 | } 11 | } 12 | ], 13 | "version" : 2 14 | } 15 | -------------------------------------------------------------------------------- /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" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "scale" : "1x", 11 | "size" : "16x16" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "scale" : "2x", 16 | "size" : "16x16" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "scale" : "1x", 21 | "size" : "32x32" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "scale" : "2x", 26 | "size" : "32x32" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "scale" : "2x", 36 | "size" : "128x128" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "scale" : "1x", 41 | "size" : "256x256" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "scale" : "2x", 46 | "size" : "256x256" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "scale" : "1x", 51 | "size" : "512x512" 52 | }, 53 | { 54 | "idiom" : "mac", 55 | "scale" : "2x", 56 | "size" : "512x512" 57 | } 58 | ], 59 | "info" : { 60 | "author" : "xcode", 61 | "version" : 1 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/Example/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // Example 4 | // 5 | // Created by p-x9 on 2023/07/25. 6 | // 7 | // 8 | 9 | import SwiftUI 10 | import EditValueView 11 | import UIKit 12 | 13 | struct ContentView: View { 14 | @State var target: Item = .init(name: "text", 15 | bool: false, 16 | date: Date(), 17 | enum: .red, 18 | enum2: .blue, 19 | color: .white) 20 | 21 | var body: some View { 22 | NavigationView { 23 | List { 24 | standardType 25 | collectionType 26 | codableType 27 | colorType 28 | imageType 29 | notSupportedType 30 | } 31 | .navigationTitle(Text("EditValueView")) 32 | } 33 | } 34 | 35 | var standardType: some View { 36 | Section { 37 | editValueView(title: "String", key: "name", keyPath: \.name) 38 | 39 | editValueView(title: "Bool", key: "bool", keyPath: \.bool) 40 | 41 | editValueView(title: "Date", key: "date", keyPath: \.date) 42 | 43 | NavigationLink("Int", destination: { 44 | EditValueView( 45 | target, 46 | key: "number", 47 | keyPath: \Item.codable.number, 48 | presentationStyle: .push 49 | ) 50 | .onUpdate { newValue in 51 | target.codable.number = newValue 52 | } 53 | .validate { newValue in 54 | newValue.isMultiple(of: 2) 55 | } 56 | }) 57 | 58 | NavigationLink("Double") { 59 | EditValueView( 60 | target, 61 | key: "double", 62 | keyPath: \Item.codable.double, 63 | presentationStyle: .push 64 | ) 65 | .onUpdate { newValue in 66 | target.codable.double = newValue 67 | } 68 | .validate { newValue in 69 | newValue > 0 70 | } 71 | } 72 | } header: { 73 | Text("Standard") 74 | } 75 | } 76 | 77 | var collectionType: some View { 78 | Section { 79 | editValueView(title: "CaseIterable", key: "enum", keyPath: \.enum) 80 | 81 | editValueView(title: "CaseIterable & RawRepresentable", key: "enum2", keyPath: \.enum2) 82 | 83 | editValueView(title: "Array", key: "array", keyPath: \.array) 84 | 85 | editValueView(title: "Dictionary", key: "dictionary", keyPath: \.dictionary) 86 | editValueView(title: "Any Dictionary", key: "anyDictionary", keyPath: \.anyDictionary) 87 | } header: { 88 | Text("Collection") 89 | } 90 | } 91 | 92 | var codableType: some View { 93 | Section { 94 | editValueView(title: "Codable", key: "codable", keyPath: \.codable) 95 | } header: { 96 | Text("Codable") 97 | } 98 | } 99 | 100 | var colorType: some View { 101 | Section { 102 | editValueView(title: "Color", key: "color", keyPath: \.color) 103 | 104 | editValueView(title: "UI/NSColor", key: "ui/nsColor", keyPath: \.uiColor) 105 | 106 | editValueView(title: "CGColor", key: "cgColor", keyPath: \.cgColor) 107 | 108 | editValueView(title: "CIColor", key: "ciColor", keyPath: \.ciColor) 109 | } header: { 110 | Text("Color") 111 | } 112 | } 113 | 114 | var imageType: some View { 115 | Section { 116 | editValueView(title: "Image", key: "image", keyPath: \.image) 117 | 118 | editValueView(title: "NS/UIImage", key: "ns/uiImage", keyPath: \.nsuiImage) 119 | 120 | editValueView(title: "CGImage", key: "cgImage", keyPath: \.cgImage) 121 | 122 | editValueView(title: "CIImage", key: "ciImage", keyPath: \.ciImage) 123 | } header: { 124 | Text("Image") 125 | } 126 | } 127 | 128 | var notSupportedType: some View { 129 | Section { 130 | NavigationLink("Not Supported") { 131 | EditValueView(key: "item", 132 | binding: $target, 133 | presentationStyle: .push) 134 | } 135 | } header: { 136 | Text("Not Supported") 137 | } 138 | } 139 | 140 | func editValueView( 141 | title: String, 142 | key: String, 143 | keyPath: WritableKeyPath 144 | ) -> NavigationLink> { 145 | NavigationLink(title) { 146 | EditValueView( 147 | target, 148 | key: key, 149 | keyPath: keyPath, 150 | presentationStyle: .push 151 | ) 152 | .onUpdate { newValue in 153 | target[keyPath: keyPath] = newValue 154 | } 155 | } 156 | } 157 | } 158 | 159 | struct ContentView_Previews: PreviewProvider { 160 | static var previews: some View { 161 | ContentView() 162 | } 163 | } 164 | 165 | enum Enum: CaseIterable { 166 | case red, yellow, blue 167 | } 168 | enum Enum2: Int, CaseIterable, DefaultRepresentable { 169 | case red, yellow, blue 170 | 171 | static var defaultValue: Self { 172 | .red 173 | } 174 | } 175 | struct Item { 176 | var name: String 177 | var bool: Bool 178 | var date: Date? 179 | var `enum`: Enum 180 | var enum2: Enum2? 181 | var color: Color 182 | var `codable`: ACodable = .init(text: "", number: 5) 183 | var array = ["AA", "BB"] 184 | var dictionary: [String: Int] = [ 185 | "AA": 0, 186 | "BB": 234 187 | ] 188 | var cgColor = UIColor.yellow.cgColor 189 | var uiColor = UIColor.blue 190 | var ciColor = CIColor(color: UIColor.brown) 191 | 192 | var image: Image = .init(systemName: "swift") 193 | var nsuiImage = UIImage(systemName: "swift") 194 | var cgImage = UIImage(systemName: "swift")?.cgImage 195 | var ciImage = UIImage(systemName: "swift")?.ciImage 196 | var anyDictionary: [String: Any] = ["AA": 0] 197 | } 198 | 199 | struct ACodable: Codable { 200 | var text: String 201 | var number: Int 202 | var double: Double = 0.4 203 | var optionalString: String? = "test" 204 | var nested: BCodable = .init(text: "", number: 0) 205 | } 206 | 207 | extension ACodable: DefaultRepresentable { 208 | static var defaultValue: ACodable = .init(text: "", number: 0) 209 | } 210 | 211 | struct BCodable: Codable { 212 | var text: String 213 | var number: Int 214 | var optionalString: String? 215 | } 216 | -------------------------------------------------------------------------------- /Example/Example/Example.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Example/Example/ExampleApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExampleApp.swift 3 | // Example 4 | // 5 | // Created by p-x9 on 2023/07/25. 6 | // 7 | // 8 | 9 | import SwiftUI 10 | 11 | @main 12 | struct ExampleApp: App { 13 | var body: some Scene { 14 | WindowGroup { 15 | ContentView() 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Example/Example/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Example/Example/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/ExampleTests/ExampleTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExampleTests.swift 3 | // Example 4 | // 5 | // Created by p-x9 on 2023/07/25. 6 | // 7 | // 8 | 9 | import XCTest 10 | 11 | final class ExampleTests: 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 | 17 | override func tearDownWithError() throws { 18 | // Put teardown code here. This method is called after the invocation of each test method in the class. 19 | } 20 | 21 | func testExample() throws { 22 | // This is an example of a functional test case. 23 | // Use XCTAssert and related functions to verify your tests produce the correct results. 24 | // Any test you write for XCTest can be annotated as throws and async. 25 | // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. 26 | // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. 27 | } 28 | 29 | func testPerformanceExample() throws { 30 | // This is an example of a performance test case. 31 | measure { 32 | // Put the code you want to measure the time of here. 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /Example/ExampleUITests/ExampleUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExampleUITests.swift 3 | // Example 4 | // 5 | // Created by p-x9 on 2023/07/25. 6 | // 7 | // 8 | 9 | import XCTest 10 | 11 | final 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 2023/07/25. 6 | // 7 | // 8 | 9 | import XCTest 10 | 11 | final 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 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "swift-magic-mirror", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/p-x9/swift-magic-mirror.git", 7 | "state" : { 8 | "revision" : "390e248dd6727e17aeb3949c12bb83e6eac876d1", 9 | "version" : "0.2.0" 10 | } 11 | }, 12 | { 13 | "identity" : "swiftui-reflection-view", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/p-x9/swiftui-reflection-view.git", 16 | "state" : { 17 | "revision" : "ce3b810ecfa1b2e308cbb5192ee4c8c43b1eefa8", 18 | "version" : "0.8.0" 19 | } 20 | }, 21 | { 22 | "identity" : "swiftuicolor", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/p-x9/SwiftUIColor.git", 25 | "state" : { 26 | "revision" : "aa42f452698cc0f78dcba2c6544f4abf7724602f", 27 | "version" : "0.4.0" 28 | } 29 | } 30 | ], 31 | "version" : 2 32 | } 33 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.7 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "EditValueView", 7 | platforms: [ 8 | .iOS(.v14), 9 | .macOS(.v11) 10 | ], 11 | products: [ 12 | .library( 13 | name: "EditValueView", 14 | targets: ["EditValueView"] 15 | ) 16 | ], 17 | dependencies: [ 18 | .package(url: "https://github.com/p-x9/SwiftUIColor.git", .upToNextMajor(from: "0.3.0")), 19 | .package(url: "https://github.com/p-x9/swift-magic-mirror.git", from: "0.2.0"), 20 | .package(url: "https://github.com/p-x9/swiftui-reflection-view.git", from: "0.8.0") 21 | ], 22 | targets: [ 23 | .target( 24 | name: "EditValueView", 25 | dependencies: [ 26 | .product(name: "SwiftUIColor", package: "SwiftUIColor"), 27 | .product(name: "MagicMirror", package: "swift-magic-mirror"), 28 | .product(name: "ReflectionView", package: "swiftui-reflection-view") 29 | ] 30 | ), 31 | .testTarget( 32 | name: "EditValueViewTests", 33 | dependencies: ["EditValueView"] 34 | ) 35 | ] 36 | ) 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EditValueView 2 | 3 | Library that makes easy to display property edit screens for SwiftUI. 4 | 5 | ## Demo 6 | 7 | | String | Bool | Int | 8 | | ---- | ---- | ---- | 9 | | ![String-light](https://user-images.githubusercontent.com/50244599/197402681-7e3c4ec8-f7c3-4ad7-9e31-8e3cd270342f.png) | ![Bool-light](https://user-images.githubusercontent.com/50244599/197402668-973d18c4-9f87-4f2f-9e6c-77072b4a8db6.png) | ![Int-light](https://user-images.githubusercontent.com/50244599/197402680-eb91f16f-52db-441a-b923-706889c256f8.png) | 10 | 11 | | Double | Date | Color | 12 | | ---- | ---- | ---- | 13 | | ![Double-light](https://user-images.githubusercontent.com/50244599/197402677-cb2a90ca-58fa-4d2d-8459-fa2539836c36.png) | ![Date-light](https://user-images.githubusercontent.com/50244599/197402673-414f5b2d-9031-4ad3-81de-300d85e5ad56.png) | ![Color-light](https://user-images.githubusercontent.com/50244599/197402671-8a224878-ab39-4471-b072-cbb19a2d38b9.png) | 14 | 15 | | Image | UI/NSImage | 16 | | ---- | ---- | 17 | | ![Image-light](https://github.com/p-x9/EditValueView/assets/50244599/792faddd-96c8-476c-8d9f-1b8c250972ee) | ![UIImage-light](https://github.com/p-x9/EditValueView/assets/50244599/e6f2a27f-5d8f-4aca-a7fd-cfe6a8c1a952) | 18 | 19 | | Array | Dictionary | 20 | | ---- | ---- | 21 | | ![Array-light](https://user-images.githubusercontent.com/50244599/197402664-fce3326c-824d-4853-9a5b-47903ccdf470.png) | ![Dictionary-light](https://user-images.githubusercontent.com/50244599/197402675-d1dd4bdb-6135-4c45-89f9-2f640daf9f3d.png) | 22 | 23 | | Enum(CaseIterable) | Enum(CaseIterable & RawRepresentable) | 24 | | ---- | ---- | 25 | | ![Enum(CaseIterable)-light](https://user-images.githubusercontent.com/50244599/197402679-c6be841f-02ca-4db6-81ba-5e5e4893058d.png) | ![Enum(CaseIterable RawRepresentable)-light](https://user-images.githubusercontent.com/50244599/197402678-dc8547ec-add7-436c-8cba-44d950f0d676.png) | 26 | 27 | | Codable | 28 | | ---- | 29 | | ![Codable-light](https://user-images.githubusercontent.com/50244599/197402669-5fe684df-cbbe-4945-b89e-264e00fed733.png) | 30 | 31 | ## Supported types 32 | - String 33 | - Bool 34 | - any Numerics 35 | - Date 36 | - Color/UIColor/NSColor/CGColor/CIColor 37 | - Image/UIImage/CGImage/CIImage (iOS Only) 38 | - Array(Codable) 39 | - Dictionary(Codable) 40 | - CaseIterable 41 | - CaseIterable & RawRepresentable 42 | - Codable 43 | 44 | ## Usage 45 | > **Note** 46 | > If you want to use the camera for editing images, you must add a key named `NSCameraUsageDescription` to the info.plist file. 47 | 48 | ### SwiftUI 49 | #### Initialize 50 | - Initialize with key and initial value 51 | ```swift 52 | var name = "" 53 | EditValueView(key: "name", value: name) 54 | .onUpdate { newValue in 55 | name = newValue 56 | } 57 | ``` 58 | - Initialize with keyPath 59 | ```swift 60 | EditValueView(target, key: "name", keyPath: \Item.name) 61 | .onUpdate { newValue in 62 | target[keyPath: \.name] = newValue 63 | } 64 | ``` 65 | - Initialize with binding 66 | ```swift 67 | @State var name: String = "" 68 | EditValueView(key: "name", binding: $name) 69 | ``` 70 | 71 | #### Update Handler 72 | You can receive an edit callback when you press the `save` button. 73 | ```swift 74 | EditValueView(target, key: "name", keyPath: \Item.name) 75 | .onUpdate { newValue in 76 | // update 77 | } 78 | ``` 79 | 80 | #### Input Validation 81 | You can validate input values. 82 | ```swift 83 | EditValueView(target, key: "name", keyPath: \Item.name) 84 | .validate { newValue -> Bool in 85 | // input validation 86 | return !name.isEmpty 87 | } 88 | ``` 89 | 90 | ### UIKit 91 | ```swift 92 | let vc = EditValueViewController(target, key: "name", keyPath: \Item.name) 93 | vc.onUpdate = { target, newValue in 94 | // update 95 | } 96 | vc.validate = { target, newValue -> Bool in 97 | // input validation 98 | } 99 | ``` 100 | 101 | ### Protocol 102 | When using optional types, type hints for Codable cannot be displayed when nil is used. 103 | To avoid such problems, provide a default value in accordance with the protocol named `DefaultRepresentable`. 104 | 105 | ```swift 106 | struct Item: Codable { 107 | var name: String 108 | var date: Date 109 | } 110 | 111 | struct Message: Codable { 112 | var content: String 113 | var item: Item? 114 | } 115 | ``` 116 | ```swift 117 | // Confirm to `DefaultRepresentable` protocol 118 | extension Item: DefaultRepresentable { 119 | static var defaultValue: Self { 120 | .init(name: "name", date: Date()) 121 | } 122 | } 123 | ``` 124 | ```swift 125 | // give default value 126 | EditValueView(target, key: "item", keyPath: \Message.item, defaultValue: .init(name: "name", date: Date())) 127 | ``` 128 | 129 | ## License 130 | EditValueView is released under the MIT License. See [LICENSE](./LICENSE) 131 | -------------------------------------------------------------------------------- /Sources/EditValueView/EditValueView+EditorType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EditValueView+EditorType.swift 3 | // 4 | // 5 | // Created by p-x9 on 2023/07/23. 6 | // 7 | // 8 | 9 | import Foundation 10 | import SwiftUI 11 | 12 | extension EditValueView { 13 | enum EditorType { 14 | enum Line { 15 | case single, multi 16 | } 17 | case string 18 | case toggle 19 | case codable(Line) 20 | case caseiterable 21 | case color 22 | case image 23 | case date 24 | case none 25 | 26 | var shouldShowOptionalEditor: Bool { 27 | switch self { 28 | case .image, .codable(.multi), .caseiterable: 29 | return false 30 | default: 31 | return true 32 | } 33 | } 34 | } 35 | 36 | var editorType: EditorType { 37 | switch Value.self { 38 | case is String.Type, 39 | is String?.Type: 40 | return .string 41 | 42 | case is Bool.Type, 43 | is Bool?.Type: 44 | return .toggle 45 | 46 | case is any Numeric.Type, 47 | is any OptionalNumeric.Type: 48 | return .codable(.single) 49 | 50 | case is Date.Type, 51 | is Date?.Type: 52 | return .date 53 | 54 | case is Color.Type, 55 | is Color?.Type: 56 | return .color 57 | 58 | case is CGColor.Type, 59 | is CGColor?.Type: 60 | return .color 61 | 62 | case is NSUIColor.Type, 63 | is NSUIColor?.Type: 64 | return .color 65 | 66 | case is CIColor.Type, 67 | is CIColor?.Type: 68 | return .color 69 | 70 | #if canImport(UIKit) 71 | case is Image.Type, 72 | is Image?.Type: 73 | return .image 74 | 75 | case is NSUIImage.Type, 76 | is NSUIImage?.Type: 77 | return .image 78 | 79 | case is CGImage.Type, 80 | is CGImage?.Type: 81 | return .image 82 | 83 | case is CIImage.Type, 84 | is CIImage?.Type: 85 | return .image 86 | #endif 87 | 88 | case is any CaseIterable.Type, 89 | is any OptionalCaseIterable.Type: 90 | return .caseiterable 91 | 92 | case is any Codable.Type: 93 | return .codable(.multi) 94 | 95 | default: 96 | return .none 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Sources/EditValueView/EditValueView+setValue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EditValueView+setValue.swift 3 | // 4 | // 5 | // Created by p-x9 on 2023/07/23. 6 | // 7 | // 8 | 9 | import Foundation 10 | import SwiftUI 11 | 12 | extension EditValueView { 13 | func setDefault() { 14 | if let type = Value.self as? any DefaultRepresentable.Type, 15 | let defaultValue = type.defaultValue as? Value { 16 | value = defaultValue 17 | } 18 | } 19 | 20 | func setNil() { 21 | guard isOptional else { return } 22 | 23 | switch $value { 24 | case let v as Binding: 25 | v.wrappedValue = nil 26 | 27 | case let v as Binding: 28 | v.wrappedValue = nil 29 | 30 | case let v as Binding: 31 | v.wrappedValue = nil 32 | 33 | case let v as Binding: 34 | v.wrappedValue = nil 35 | 36 | case let v as Binding: 37 | v.wrappedValue = nil 38 | 39 | case let v as Binding: 40 | v.wrappedValue = nil 41 | 42 | case let v as Binding: 43 | v.wrappedValue = nil 44 | 45 | #if canImport(UIKit) 46 | case let v as Binding: 47 | v.wrappedValue = nil 48 | 49 | case let v as Binding: 50 | v.wrappedValue = nil 51 | 52 | case let v as Binding: 53 | v.wrappedValue = nil 54 | 55 | case let v as Binding: 56 | v.wrappedValue = nil 57 | #endif 58 | 59 | case _ where Value.self is any Codable.Type, 60 | _ where Value.self is any OptionalNumeric.Type: 61 | if let type = Value.self as? any Codable.Type, 62 | let nilValue = type.value(from: "null") as? Value { 63 | value = nilValue 64 | } 65 | 66 | default: 67 | break 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Sources/EditValueView/EditValueView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EditValueView.swift 3 | // 4 | // 5 | // Created by p-x9 on 2022/10/16. 6 | // 7 | // 8 | import SwiftUI 9 | import SwiftUIColor 10 | 11 | @available(iOS 14, *) 12 | public struct EditValueView: View { 13 | public enum PresentationStyle { 14 | case modal 15 | case push 16 | } 17 | 18 | /// Name of the property to be edited 19 | /// Used for navigation titles and type descriptions. 20 | let key: String 21 | 22 | /// Presentation style. 23 | /// If set to `modal`, it will be wrapped in NavigationView 24 | /// On the other hand, if `push` is specified, it is not wrapped in NavigationView and must contain NavigationView as its parent. 25 | var presentationStyle: PresentationStyle 26 | 27 | /// This is called when editing is completed by pressing the save button. 28 | /// They will be received by a modifier named `onUpdate`. 29 | private var _onUpdate: ((Value) -> Void)? 30 | 31 | /// Used to perform validation checks when editing values 32 | /// They will be received by a modifier named `validate`. 33 | private var _validate: ((Value) -> Bool)? 34 | 35 | /// Value to be edited 36 | @State var value: Value 37 | 38 | /// True if nil set is requested by optionalEditor. 39 | /// Note: Use the `isNil` property to check if a value is actually nil. 40 | @State private var shouldSetNil = false 41 | 42 | /// A boolean value indicating whether the current input value can be converted to the actual type. 43 | @State private var isValidType = true 44 | 45 | /// binding received from the initializer. 46 | /// When the save button is pressed, the binding is updated to indicate the value change. 47 | private var binding: Binding? 48 | 49 | /// A boolean value that indicates whether the current input value can be converted to the actual type and conforms to the validation checks 50 | var isValid: Bool { 51 | isValidType && (_validate?(value) ?? true) 52 | } 53 | 54 | /// A boolean value that indicates whether the type being edited is an Optional type or not 55 | var isOptional: Bool { 56 | Value.self is any OptionalType.Type 57 | } 58 | 59 | /// A boolean value that indicates whether the value is nil 60 | var isNil: Bool { 61 | if let optional = value as? any OptionalType { 62 | return optional.wrapped == nil 63 | } 64 | return false 65 | } 66 | 67 | /// A boolean value that indicates whether optionalEditor should be displayed 68 | var shouldShowOptionalEditor: Bool { 69 | isOptional && 70 | editorType.shouldShowOptionalEditor 71 | } 72 | 73 | @Environment(\.presentationMode) private var presentationMode 74 | 75 | public var body: some View { 76 | if presentationStyle == .push { 77 | _body 78 | } else { 79 | NavigationView { 80 | _body 81 | } 82 | } 83 | } 84 | 85 | @MainActor 86 | var _body: some View { 87 | GeometryReader{ proxy in 88 | ScrollView { 89 | VStack(alignment: .leading, spacing: 8) { 90 | header 91 | 92 | typeSection 93 | .padding(.top) 94 | 95 | if isOptional && shouldShowOptionalEditor { 96 | optionalEditor 97 | .padding() 98 | .border(Color.iOS(.label), width: 0.5) 99 | .padding(.vertical) 100 | } 101 | 102 | if !shouldSetNil && !isNil || !shouldShowOptionalEditor { 103 | editor 104 | .padding(.vertical) 105 | .layoutPriority(.infinity) 106 | } 107 | 108 | Spacer() 109 | } 110 | .padding() 111 | .frame(minHeight: proxy.size.height) 112 | .navigationTitle(key) 113 | .toolbar { 114 | ToolbarItem(placement: .destructiveAction) { 115 | Button("Save") { 116 | save() 117 | presentationMode.wrappedValue.dismiss() 118 | } 119 | .disabled(!isValid) 120 | } 121 | ToolbarItem(placement: .cancellationAction) { 122 | if presentationStyle != .push { 123 | Button("Cancel") { 124 | presentationMode.wrappedValue.dismiss() 125 | } 126 | } 127 | } 128 | } 129 | } 130 | } 131 | } 132 | 133 | /// Displays key and type information 134 | @ViewBuilder 135 | var header: some View { 136 | VStack(alignment: .leading, spacing: 8) { 137 | HStack { 138 | let string: String = "Key: \(key)" 139 | Text(string) 140 | .font(.system(size: 14, weight: .bold, design: .monospaced)) 141 | .foregroundColor(.gray) 142 | Spacer() 143 | } 144 | HStack { 145 | let string: String = "Type: \(Value.self)" 146 | Text(string) 147 | .font(.system(size: 14, weight: .bold, design: .monospaced)) 148 | .foregroundColor(.gray) 149 | Spacer() 150 | } 151 | } 152 | } 153 | 154 | /// Displays type information 155 | @ViewBuilder 156 | var typeSection: some View { 157 | HStack { 158 | let type = "\(Value.self)" 159 | Text(type) 160 | .font(.system(size: 14, weight: .bold, design: .monospaced)) 161 | .foregroundColor(.gray) 162 | Spacer() 163 | } 164 | .padding() 165 | .background(Color.iOS.secondarySystemFill) 166 | .cornerRadius(8) 167 | } 168 | 169 | @ViewBuilder 170 | var optionalEditor: some View { 171 | Toggle(isOn: $shouldSetNil) { 172 | Text("Set value to `nil`") 173 | } 174 | .onChange(of: shouldSetNil) { newValue in 175 | if newValue { 176 | setNil() 177 | } else { 178 | setDefault() 179 | } 180 | } 181 | } 182 | 183 | @ViewBuilder 184 | var notSupportedView: some View { 185 | Text("this type is currently not supported.") 186 | } 187 | 188 | @ViewBuilder 189 | var editor: some View { 190 | switch $value { 191 | case let v as Binding: 192 | TextEditor(text: v) 193 | .frame(minHeight: 200, maxHeight: .infinity) 194 | .border(Color.iOS(.label), width: 0.5) 195 | 196 | case let v as Binding: 197 | Toggle(key, isOn: v) 198 | .padding() 199 | .border(Color.iOS(.label), width: 0.5) 200 | 201 | case _ where Value.self is any Numeric.Type: 202 | CodableEditorView($value, key: key, isValidType: $isValidType, textStyle: .single) 203 | 204 | case let v as Binding: 205 | DateEditorView(v, key: key) 206 | 207 | case let v as Binding: 208 | ColorEditorView(v, key: key) 209 | 210 | case let v as Binding: 211 | ColorEditorView(v, key: key) 212 | 213 | case let v as Binding: 214 | ColorEditorView(v, key: key) 215 | 216 | case let v as Binding: 217 | ColorEditorView(v, key: key) 218 | 219 | #if canImport(UIKit) 220 | case let v as Binding: 221 | SwiftUIImageEditor(image: .init(v), isOptional: false) 222 | 223 | case let v as Binding: 224 | ImageEditor(v) 225 | 226 | case let v as Binding: 227 | ImageEditor(v) 228 | 229 | case let v as Binding: 230 | ImageEditor(v) 231 | #endif 232 | 233 | case _ where Value.self is any CaseIterable.Type: 234 | CaseIterableEditor($value, key: key) 235 | .border(Color.iOS(.label), width: 0.5) 236 | 237 | /* Optional Type */ 238 | case let v as Binding where !isNil: 239 | TextEditor(text: Binding(v)!) 240 | .frame(minHeight: 200, maxHeight: .infinity) 241 | .border(Color.iOS(.label), width: 0.5) 242 | 243 | case let v as Binding where !isNil: 244 | Toggle(key, isOn: Binding(v)!) 245 | .padding() 246 | .border(Color.iOS(.label), width: 0.5) 247 | 248 | case _ where Value.self is any OptionalNumeric.Type: 249 | CodableEditorView($value, key: key, isValidType: $isValidType, textStyle: .single) 250 | 251 | case let v as Binding where !isNil: 252 | DateEditorView(Binding(v)!, key: key) 253 | 254 | case let v as Binding where !isNil: 255 | ColorEditorView(Binding(v)!, key: key) 256 | 257 | case let v as Binding where !isNil: 258 | ColorEditorView(Binding(v)!, key: key) 259 | 260 | case let v as Binding where !isNil: 261 | ColorEditorView(Binding(v)!, key: key) 262 | 263 | case let v as Binding where !isNil: 264 | ColorEditorView(Binding(v)!, key: key) 265 | 266 | #if canImport(UIKit) 267 | case let v as Binding: 268 | SwiftUIImageEditor(image: v, isOptional: true) 269 | 270 | case let v as Binding: 271 | ImageEditor(v) 272 | 273 | case let v as Binding: 274 | ImageEditor(v) 275 | 276 | case let v as Binding: 277 | ImageEditor(v) 278 | #endif 279 | 280 | case _ where Value.self is any OptionalCaseIterable.Type: 281 | CaseIterableEditor($value, key: key) 282 | .border(Color.iOS(.label), width: 0.5) 283 | 284 | /* Other */ 285 | case _ where Value.self is any Codable.Type: 286 | CodableEditorView($value, key: key, isValidType: $isValidType) 287 | 288 | default: 289 | if let anyJSONEditor = AnyJSONEditor($value, key: key, isValidType: $isValidType) { 290 | anyJSONEditor 291 | } else { 292 | notSupportedView 293 | } 294 | } 295 | } 296 | 297 | /// Set the process to be performed when the Save button is pressed. 298 | /// - Parameter onUpdate: A closure that receives the edited value 299 | /// - Returns: Self 300 | public func onUpdate(_ onUpdate: ((Value) -> Void)?) -> Self { 301 | var new = self 302 | new._onUpdate = onUpdate 303 | return new 304 | } 305 | 306 | /// Set validation checks. 307 | /// Returning false disables the save button 308 | /// - Parameter validate: A closure that takes the current value after editing and returns the validation result 309 | /// - Returns: Self 310 | public func validate(_ validate: ((Value) -> Bool)?) -> Self { 311 | var new = self 312 | new._validate = validate 313 | return new 314 | } 315 | 316 | private func save() { 317 | binding?.wrappedValue = value 318 | _onUpdate?(value) 319 | } 320 | } 321 | 322 | extension EditValueView { 323 | /// Initialize with key and value 324 | /// - Parameters: 325 | /// - key: Name of the property to be edited. Used for navigation titles and type descriptions. 326 | /// - value: Initial value of the value to be edited 327 | /// - presentationStyle: Presentation style. If set to `modal`, 328 | /// it will be wrapped in NavigationView. 329 | /// On the other hand, if `push` is specified, it is not wrapped in NavigationView and must contain NavigationView as its parent. 330 | public init(key: String, value: Value, presentationStyle: PresentationStyle = .modal) { 331 | self.key = key 332 | self._value = .init(initialValue: value) 333 | self.presentationStyle = presentationStyle 334 | 335 | self._shouldSetNil = .init(initialValue: isNil) 336 | } 337 | 338 | /// Initialize with keyPath 339 | /// - Parameters: 340 | /// - target: Target object that has the property to be edited. 341 | /// - key: Name of the property to be edited. Used for navigation titles and type descriptions. 342 | /// - keyPath: keyPath of the property to be edited. 343 | /// - presentationStyle: Presentation style. If set to `modal`, 344 | /// it will be wrapped in NavigationView. 345 | /// On the other hand, if `push` is specified, it is not wrapped in NavigationView and must contain NavigationView as its parent. 346 | public init(_ target: Root, key: String, keyPath: WritableKeyPath, presentationStyle: PresentationStyle = .modal) { 347 | self.init(key: key, 348 | value: target[keyPath: keyPath], 349 | presentationStyle: presentationStyle) 350 | } 351 | 352 | /// Initialize with binding 353 | /// - Parameters: 354 | /// - key: Name of the property to be edited. Used for navigation titles and type descriptions. 355 | /// - binding: Binder for the value to be edited 356 | /// - presentationStyle: Presentation style. If set to `modal`, 357 | /// it will be wrapped in NavigationView. 358 | /// On the other hand, if `push` is specified, it is not wrapped in NavigationView and must contain NavigationView as its parent. 359 | public init(key: String, binding: Binding, presentationStyle: PresentationStyle = .modal) { 360 | self.key = key 361 | self._value = .init(initialValue: binding.wrappedValue) 362 | self.binding = binding 363 | self.presentationStyle = presentationStyle 364 | 365 | self._shouldSetNil = .init(initialValue: isNil) 366 | } 367 | } 368 | 369 | extension EditValueView { 370 | public func presentationStyle(_ style: PresentationStyle) -> Self { 371 | set(style, for: \.presentationStyle) 372 | } 373 | } 374 | 375 | #if DEBUG 376 | enum Enum: CaseIterable { 377 | case red, yellow, blue 378 | } 379 | enum Enum2: Int, CaseIterable, DefaultRepresentable { 380 | case red, yellow, blue 381 | 382 | static var defaultValue: Self { 383 | .red 384 | } 385 | } 386 | struct Item { 387 | var name: String 388 | var bool: Bool 389 | var date: Date 390 | var `enum`: Enum 391 | var enum2: Enum2? 392 | var image: Image = .init(systemName: "swift") 393 | var color: Color 394 | var `codable`: ACodable = .init(text: "", number: 5) 395 | var array = ["AA", "BB"] 396 | var dictionary: [String: Int] = [ 397 | "AA": 0, 398 | "BB": 234 399 | ] 400 | var anyDictionary: [String: Any] = ["AA": 0] 401 | 402 | var cgColor = NSUIColor.yellow.cgColor 403 | var uiColor = NSUIColor.blue 404 | var ciColor = CIColor(color: NSUIColor.brown) 405 | 406 | #if canImport(UIKit) 407 | var nsuiImage = NSUIImage(systemName: "swift") 408 | var cgImage = NSUIImage(systemName: "swift")?.cgImage 409 | var ciImage = NSUIImage(systemName: "swift")?.ciImage 410 | #endif 411 | } 412 | 413 | struct ACodable: Codable { 414 | var text: String 415 | var number: Int 416 | var double: Double = 0.4 417 | var optionalString: String? = "test" 418 | var nested: BCodable = .init(text: "", number: 0) 419 | } 420 | 421 | extension ACodable: DefaultRepresentable { 422 | static var defaultValue: ACodable = .init(text: "", number: 0) 423 | } 424 | 425 | struct BCodable: Codable { 426 | var text: String 427 | var number: Int 428 | var optionalString: String? 429 | } 430 | 431 | let target: Item = .init(name: "Hello!!", 432 | bool: true, 433 | date: Date(), 434 | enum: .red, 435 | enum2: .blue, 436 | color: .white) 437 | 438 | struct EditValueView_Preview: PreviewProvider { 439 | static var previews: some View { 440 | Group { 441 | Group { 442 | EditValueView(target, key: "name", keyPath: \Item.name) 443 | .validate { value in 444 | value != "Test" 445 | } 446 | .previewDisplayName("String") 447 | 448 | EditValueView(target, key: "bool", keyPath: \Item.bool) 449 | .previewDisplayName("Bool") 450 | 451 | EditValueView(target, key: "date", keyPath: \Item.date) 452 | .previewDisplayName("Date") 453 | 454 | EditValueView(target, key: "number", keyPath: \Item.codable.number) 455 | .previewDisplayName("Int") 456 | 457 | EditValueView(target, key: "double", keyPath: \Item.codable.double) 458 | .previewDisplayName("Double") 459 | } 460 | 461 | Group { 462 | EditValueView(target, key: "enum", keyPath: \Item.enum) 463 | .previewDisplayName("Enum(CaseIterable)") 464 | 465 | EditValueView(target, key: "enum", keyPath: \Item.enum2) 466 | .previewDisplayName("Enum(CaseIterable & RawRepresentable)") 467 | 468 | EditValueView(target, key: "array", keyPath: \Item.array) 469 | .previewDisplayName("Array") 470 | 471 | EditValueView(target, key: "dictionary", keyPath: \Item.dictionary) 472 | .previewDisplayName("Dictionary") 473 | } 474 | 475 | EditValueView(target, key: "codable", keyPath: \Item.codable) 476 | .previewDisplayName("Codable") 477 | 478 | EditValueView(target, key: "anyDictionary", keyPath: \Item.anyDictionary) 479 | .previewDisplayName("Any Dictionary") 480 | } 481 | } 482 | } 483 | 484 | struct EditValueView_Color_Preview: PreviewProvider { 485 | static var previews: some View { 486 | Group { 487 | EditValueView(target, key: "color", keyPath: \Item.color) 488 | .previewDisplayName("Color") 489 | 490 | EditValueView(target, key: "ui/nsColor", keyPath: \Item.uiColor) 491 | .previewDisplayName("UI/NSColor") 492 | 493 | EditValueView(target, key: "cgColor", keyPath: \Item.cgColor) 494 | .previewDisplayName("CGColor") 495 | 496 | EditValueView(target, key: "ciColor", keyPath: \Item.ciColor) 497 | .previewDisplayName("CIColor") 498 | } 499 | } 500 | } 501 | 502 | #if canImport(UIKit) 503 | struct EditValueView_Image_Preview: PreviewProvider { 504 | static var previews: some View { 505 | Group { 506 | EditValueView(target, key: "image", keyPath: \Item.image) 507 | .previewDisplayName("Image") 508 | 509 | EditValueView(target, key: "ns/uiImage", keyPath: \Item.nsuiImage) 510 | .previewDisplayName("NS/UIImage") 511 | 512 | EditValueView(target, key: "cgImage", keyPath: \Item.cgImage) 513 | .previewDisplayName("CGImage") 514 | 515 | EditValueView(target, key: "ciImage", keyPath: \Item.ciImage) 516 | .previewDisplayName("CIImage") 517 | } 518 | } 519 | } 520 | #endif 521 | #endif 522 | -------------------------------------------------------------------------------- /Sources/EditValueView/Editors/AnyJSONEditor.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import SwiftUIColor 3 | 4 | struct AnyJSONEditor: View { 5 | let key: String 6 | 7 | @Binding private var value: Value 8 | @Binding private var isValidType: Bool 9 | 10 | @State private var text: String 11 | 12 | init?(_ value: Binding, key: String, isValidType: Binding) { 13 | self._value = value 14 | self._isValidType = isValidType 15 | self.key = key 16 | 17 | guard JSONSerialization.isValidJSONObject(value.wrappedValue), 18 | let json = try? JSONSerialization.data(withJSONObject: value.wrappedValue, options: [.prettyPrinted, .sortedKeys]), 19 | let jsonString = String(data: json, encoding: .utf8) 20 | else { 21 | return nil 22 | } 23 | 24 | self._text = .init(initialValue: jsonString) 25 | } 26 | 27 | var body: some View { 28 | VStack { 29 | editor 30 | .onChange(of: text) { newValue in 31 | textChanged(text: newValue) 32 | } 33 | } 34 | } 35 | 36 | @ViewBuilder 37 | var editor: some View { 38 | TextEditor(text: $text) 39 | .frame(minHeight: 200, maxHeight: .infinity) 40 | .border(Color.iOS(.label), width: 0.5) 41 | .padding(.vertical) 42 | } 43 | 44 | func textChanged(text: String) { 45 | guard let data = text.data(using: .utf8), 46 | let value = try? JSONSerialization.jsonObject(with: data) as? Value 47 | else { 48 | isValidType = false 49 | return 50 | } 51 | self.value = value 52 | isValidType = true 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Sources/EditValueView/Editors/CaseIterableEditor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CaseIterableEditor.swift 3 | // 4 | // 5 | // Created by p-x9 on 2022/10/20. 6 | // 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct CaseIterableEditor: View { 12 | 13 | let key: String 14 | @Binding private var value: Value 15 | @State private var index: Int = 0 16 | 17 | private let allCases: [Value] 18 | 19 | var isNil: Bool { 20 | if let optional = value as? any OptionalType { 21 | return optional.wrapped == nil 22 | } 23 | return false 24 | } 25 | 26 | init(_ value: Binding, key: String) { 27 | self._value = value 28 | self.key = key 29 | 30 | switch Value.self { 31 | case let type as any CaseIterable.Type: 32 | self.allCases = type.allCases as! [Value] 33 | case let type as any OptionalCaseIterable.Type: 34 | self.allCases = type.optionalAllCases as! [Value] 35 | default: 36 | fatalError() 37 | } 38 | 39 | self._index = .init(wrappedValue: allCases.firstIndex(where: { "\($0)" == "\(value.wrappedValue)" }) ?? 0) 40 | } 41 | 42 | var body: some View { 43 | VStack { 44 | if allCases.isEmpty { 45 | Text("this type is currently not supported.") 46 | } else { 47 | editor 48 | } 49 | } 50 | .onChange(of: index) { newValue in 51 | value = allCases[newValue] 52 | } 53 | } 54 | 55 | @ViewBuilder 56 | var editor: some View { 57 | switch Value.self { 58 | case let type as any (CaseIterable & RawRepresentable).Type: 59 | let allCases = type.allCases as! [Value] 60 | Picker(key, selection: $index) { 61 | ForEach(0..: View { 14 | 15 | enum TextStyle { 16 | case single 17 | case multiline 18 | } 19 | 20 | let key: String 21 | 22 | @Binding private var value: Value 23 | @Binding private var isValidType: Bool 24 | 25 | @State private var text: String 26 | @State private var textStyle: TextStyle 27 | 28 | init(_ value: Binding, key: String, isValidType: Binding, textStyle: TextStyle = .multiline) { 29 | self._value = value 30 | self._isValidType = isValidType 31 | self.key = key 32 | 33 | self._textStyle = .init(initialValue: textStyle) 34 | 35 | let codableValue = value.wrappedValue as! Codable 36 | self._text = .init(initialValue: codableValue.jsonString ?? "") 37 | } 38 | 39 | var body: some View { 40 | VStack { 41 | if textStyle == .multiline { 42 | typeDescriptionView 43 | .layoutPriority(1) 44 | } 45 | 46 | editor 47 | .onChange(of: text) { newValue in 48 | textChanged(text: newValue) 49 | } 50 | } 51 | } 52 | 53 | @ViewBuilder 54 | var typeDescriptionView: some View { 55 | HStack(alignment: .center, spacing: 0) { 56 | TypeInfoView(valueForTypeDescription) 57 | .frame(maxWidth: .infinity, alignment: .topLeading) 58 | Spacer() 59 | } 60 | .font(.system(size: 14, design: .monospaced)) 61 | .background(Color.iOS.secondarySystemFill) 62 | .cornerRadius(8) 63 | } 64 | 65 | @ViewBuilder 66 | var editor: some View { 67 | switch textStyle { 68 | case .single: 69 | TextField("", text: $text) 70 | .padding() 71 | .border(Color.iOS(.label), width: 0.5) 72 | 73 | case .multiline: 74 | TextEditor(text: $text) 75 | .frame(minHeight: 200, maxHeight: .infinity) 76 | .border(Color.iOS(.label), width: 0.5) 77 | .padding(.vertical) 78 | } 79 | } 80 | 81 | func textChanged(text: String) { 82 | let type = Value.self as! Codable.Type 83 | guard let value = type.value(from: text) as? Value else { 84 | isValidType = false 85 | return 86 | } 87 | self.value = value 88 | isValidType = true 89 | } 90 | } 91 | 92 | extension CodableEditorView { 93 | var valueForTypeDescription: Any { 94 | if let type = Value.self as? any DefaultRepresentable.Type, 95 | let value = type.defaultValue as? Value { 96 | return value 97 | } 98 | return value 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /Sources/EditValueView/Editors/ColorEditorView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ColorEditorView.swift 3 | // 4 | // 5 | // Created by p-x9 on 2022/10/30. 6 | // 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct ColorEditorView: View { 12 | 13 | let key: String 14 | 15 | @Binding private var value: Value 16 | 17 | @State private var color: CGColor 18 | 19 | init(_ value: Binding, key: String) { 20 | self._value = value 21 | self.key = key 22 | 23 | _color = .init(initialValue: Self.toCGColor(value.wrappedValue) ?? .defaultValue) 24 | } 25 | 26 | var body: some View { 27 | editor 28 | .onChange(of: color) { newValue in 29 | colorChanged(newValue) 30 | } 31 | } 32 | 33 | @ViewBuilder 34 | var editor: some View { 35 | ColorPicker(key, selection: $color) 36 | .padding() 37 | .border(Color.iOS(.label), width: 0.5) 38 | } 39 | 40 | func colorChanged(_ cgColor: CGColor) { 41 | switch Value.self { 42 | case is NSUIColor.Type: 43 | let v = NSUIColor(cgColor: cgColor) 44 | value = v as! Value 45 | break 46 | 47 | case is Color.Type: 48 | let v: Color 49 | if #available(iOS 15.0, macOS 12.0, *) { 50 | v = Color(cgColor: cgColor) 51 | } else { 52 | v = Color(cgColor) 53 | } 54 | value = v as! Value 55 | 56 | case is CIColor.Type: 57 | let v = CIColor(cgColor: cgColor) 58 | value = v as! Value 59 | 60 | case is CGColor.Type: 61 | value = cgColor as! Value 62 | 63 | default: 64 | break 65 | } 66 | } 67 | 68 | static func toCGColor(_ value: Value) -> CGColor? { 69 | switch value { 70 | case let v as NSUIColor: 71 | return v.cgColor 72 | 73 | case let v as CIColor: 74 | return NSUIColor(ciColor: v).cgColor 75 | 76 | case let v as Color: 77 | return v.cgColor 78 | 79 | case let v as CGColor where Value.self is CGColor.Type: // Conditional downcast to CoreFoundation type 'CGColor' will always succeed 80 | return v.copy() 81 | 82 | default: 83 | return nil 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Sources/EditValueView/Editors/DateEditorView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DateEditorView.swift 3 | // 4 | // 5 | // Created by p-x9 on 2022/10/21. 6 | // 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct DateEditorView: View { 12 | 13 | let key: String 14 | @Binding private var value: Date 15 | 16 | init(_ value: Binding, key: String) { 17 | _value = value 18 | self.key = key 19 | } 20 | 21 | var body: some View { 22 | VStack { 23 | Text(value.description) 24 | .frame(maxWidth: .infinity) 25 | .border(Color.iOS(.label), width: 0.5) 26 | DatePicker(key, selection: $value) 27 | .datePickerStyle(.graphical) 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/EditValueView/Editors/Image/ImageEditor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageEditor.swift 3 | // 4 | // 5 | // Created by p-x9 on 2023/07/25. 6 | // 7 | // 8 | 9 | import SwiftUI 10 | 11 | #if canImport(UIKit) 12 | struct ImageEditor: View { 13 | enum SourceType: Int, Identifiable { 14 | case library 15 | case camera 16 | 17 | var id: Int { rawValue } 18 | 19 | var type: UIImagePickerController.SourceType { 20 | switch self { 21 | case .library: return .photoLibrary 22 | case .camera: return .camera 23 | } 24 | } 25 | } 26 | 27 | @Binding var value: Value 28 | 29 | @State var image: NSUIImage? 30 | @State var sourceTypeOfPicker: SourceType? 31 | @State var isPresentedActionSheet = false 32 | @State var isPresentedFileImporter = false 33 | 34 | /// A boolean value that indicates whether the type being edited is an Optional type or not 35 | var isOptional: Bool { 36 | Value.self is any OptionalType.Type 37 | } 38 | 39 | init(_ value: Binding) { 40 | self._value = value 41 | 42 | let nsuiImage = Self.toNSUIImage(value.wrappedValue) 43 | _image = .init(initialValue: nsuiImage) 44 | } 45 | 46 | var body: some View { 47 | VStack { 48 | if image != nil { 49 | infoSection 50 | } 51 | 52 | HStack { 53 | Text("tap to select image") 54 | .foregroundColor(.gray) 55 | .font(.footnote) 56 | .padding(.top) 57 | Spacer() 58 | } 59 | 60 | editor 61 | .onTapGesture { 62 | isPresentedActionSheet = true 63 | } 64 | .onChange(of: image) { newValue in 65 | imageChanged(newValue) 66 | } 67 | .onDrop(of: [.image], isTargeted: nil, perform: handleDropImage(providers:)) 68 | } 69 | .sheet(item: $sourceTypeOfPicker) { type in 70 | ImagePicker(sourceType: type.type, selectedImage: $image) 71 | } 72 | .actionSheet(isPresented: $isPresentedActionSheet) { 73 | actionSheet 74 | } 75 | .fileImporter( 76 | isPresented: $isPresentedFileImporter, 77 | allowedContentTypes: [.image], 78 | allowsMultipleSelection: false 79 | ) { result in 80 | if case let .success(urls) = result, 81 | let url = urls.first, 82 | url.startAccessingSecurityScopedResource() { 83 | image = NSUIImage(contentsOfFile: url.path) 84 | } 85 | } 86 | } 87 | 88 | @ViewBuilder 89 | var infoSection: some View { 90 | if let image { 91 | VStack(alignment: .leading, spacing: 8) { 92 | let size = """ 93 | width: \(String(format: "%.1f", image.size.width)) 94 | height: \(String(format: "%.1f", image.size.height)) 95 | """ 96 | HStack { 97 | Text(size) 98 | .font(.system(size: 14, weight: .bold, design: .monospaced)) 99 | .foregroundColor(.gray) 100 | Spacer() 101 | } 102 | } 103 | .padding() 104 | .background(Color.iOS.secondarySystemFill) 105 | .cornerRadius(8) 106 | } else { 107 | EmptyView() 108 | } 109 | } 110 | 111 | @ViewBuilder 112 | var editor: some View { 113 | if let image { 114 | Image(uiImage: image) 115 | .renderingMode(.original) 116 | .resizable() 117 | .scaledToFit() 118 | .padding() 119 | .border(Color.iOS(.label), width: 0.5) 120 | } else if isOptional { 121 | Color.iOS.secondarySystemFill 122 | .overlay( 123 | Text("current value is nil") 124 | .foregroundColor(.gray) 125 | ) 126 | } else { 127 | Text("this type is currently not supported.") 128 | } 129 | } 130 | 131 | var actionSheet: ActionSheet { 132 | var buttons: [ActionSheet.Button] = [ 133 | .default(Text("Photo Library")) { 134 | sourceTypeOfPicker = .library 135 | }, 136 | .default(Text("Camera")) { 137 | sourceTypeOfPicker = .camera 138 | }, 139 | .default(Text("File")) { 140 | isPresentedFileImporter = true 141 | }, 142 | .destructive(Text("Set nil")) { 143 | image = nil 144 | }, 145 | .cancel() 146 | ] 147 | 148 | if !isOptional { 149 | buttons.remove(at: buttons.count - 2) 150 | } 151 | 152 | return ActionSheet( 153 | title: Text("Select source"), 154 | buttons: buttons 155 | ) 156 | } 157 | } 158 | 159 | extension ImageEditor { 160 | @MainActor 161 | func imageChanged(_ nsuiImage: NSUIImage?) { 162 | guard let nsuiImage else { 163 | setNil() 164 | return 165 | } 166 | 167 | switch $value { 168 | case let v as Binding: 169 | v.wrappedValue = nsuiImage 170 | 171 | case let v as Binding: 172 | if let ciImage = nsuiImage.toCIImage() { 173 | v.wrappedValue = ciImage 174 | } 175 | 176 | case let v as Binding where Value.self is CGImage.Type: 177 | if let cgImage = nsuiImage.cgImage { 178 | v.wrappedValue = cgImage 179 | } 180 | 181 | case let v as Binding: 182 | v.wrappedValue = nsuiImage 183 | 184 | case let v as Binding: 185 | v.wrappedValue = nsuiImage.toCIImage() 186 | 187 | case let v as Binding where Value.self is CGImage?.Type: 188 | v.wrappedValue = nsuiImage.cgImage 189 | 190 | default: 191 | break 192 | } 193 | } 194 | 195 | @MainActor 196 | func setNil() { 197 | guard isOptional else { return } 198 | 199 | switch $value { 200 | case let v as Binding: 201 | v.wrappedValue = nil 202 | case let v as Binding: 203 | v.wrappedValue = nil 204 | case let v as Binding: 205 | v.wrappedValue = nil 206 | default: 207 | break 208 | } 209 | } 210 | } 211 | 212 | extension ImageEditor { 213 | static func toNSUIImage(_ value: Value) -> NSUIImage? { 214 | switch value { 215 | case let v as NSUIImage: 216 | return v 217 | 218 | case let v as CIImage: 219 | return v.uiImage 220 | 221 | case let v as CGImage where Value.self is CGImage.Type: 222 | return .init(cgImage: v) 223 | 224 | case let v as CGImage where Value.self is CGImage?.Type: 225 | return .init(cgImage: v) 226 | 227 | default: 228 | return nil 229 | } 230 | } 231 | } 232 | 233 | extension ImageEditor { 234 | func handleDropImage(providers: [NSItemProvider]) -> Bool { 235 | guard let provider = providers.first else { return false } 236 | 237 | provider.loadObject(ofClass: NSUIImage.self) { item, error in 238 | guard let image = item as? NSUIImage else { return } 239 | DispatchQueue.main.async { 240 | self.image = image 241 | } 242 | } 243 | 244 | return true 245 | } 246 | } 247 | 248 | #endif 249 | -------------------------------------------------------------------------------- /Sources/EditValueView/Editors/Image/Picker/ImagePicker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImagePicker.swift 3 | // 4 | // 5 | // Created by p-x9 on 2023/07/25. 6 | // 7 | // 8 | 9 | #if canImport(UIKit) 10 | import SwiftUI 11 | import UIKit 12 | 13 | struct ImagePicker: UIViewControllerRepresentable { 14 | typealias UIViewControllerType = UIImagePickerController 15 | 16 | var sourceType: UIImagePickerController.SourceType = .photoLibrary 17 | 18 | @Binding var selectedImage: NSUIImage? 19 | @Environment(\.presentationMode) private var presentationMode 20 | 21 | func makeUIViewController(context: UIViewControllerRepresentableContext) -> UIViewControllerType { 22 | 23 | let imagePicker = UIImagePickerController() 24 | imagePicker.allowsEditing = false 25 | imagePicker.sourceType = sourceType 26 | imagePicker.delegate = context.coordinator 27 | 28 | return imagePicker 29 | } 30 | 31 | func updateUIViewController(_ uiViewController: UIViewControllerType, context: UIViewControllerRepresentableContext) { 32 | 33 | } 34 | 35 | func makeCoordinator() -> Coordinator { 36 | return Coordinator(self) 37 | } 38 | 39 | final class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate { 40 | 41 | var parent: ImagePicker 42 | 43 | init(_ parent: ImagePicker) { 44 | self.parent = parent 45 | } 46 | 47 | func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { 48 | 49 | if let image = info[.originalImage] as? UIImage { 50 | parent.selectedImage = image 51 | } 52 | 53 | parent.presentationMode.wrappedValue.dismiss() 54 | } 55 | } 56 | } 57 | #endif 58 | -------------------------------------------------------------------------------- /Sources/EditValueView/Editors/Image/Picker/SwiftUIImagePicker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftUIImagePicker.swift 3 | // 4 | // 5 | // Created by p-x9 on 2023/07/25. 6 | // 7 | // 8 | 9 | #if canImport(UIKit) 10 | 11 | import SwiftUI 12 | import UIKit 13 | 14 | struct SwiftUIImagePicker: UIViewControllerRepresentable { 15 | typealias UIViewControllerType = UIImagePickerController 16 | 17 | var sourceType: UIImagePickerController.SourceType = .photoLibrary 18 | 19 | @Binding var selectedImage: Image? 20 | @Environment(\.presentationMode) private var presentationMode 21 | 22 | func makeUIViewController(context: UIViewControllerRepresentableContext) -> UIViewControllerType { 23 | 24 | let imagePicker = UIImagePickerController() 25 | imagePicker.allowsEditing = false 26 | imagePicker.sourceType = sourceType 27 | imagePicker.delegate = context.coordinator 28 | 29 | return imagePicker 30 | } 31 | 32 | func updateUIViewController(_ uiViewController: UIViewControllerType, context: UIViewControllerRepresentableContext) { 33 | 34 | } 35 | 36 | func makeCoordinator() -> Coordinator { 37 | return Coordinator(self) 38 | } 39 | 40 | final class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate { 41 | 42 | var parent: SwiftUIImagePicker 43 | 44 | init(_ parent: SwiftUIImagePicker) { 45 | self.parent = parent 46 | } 47 | 48 | func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { 49 | 50 | if let image = info[.originalImage] as? UIImage { 51 | parent.selectedImage = Image(uiImage: image) 52 | } 53 | 54 | parent.presentationMode.wrappedValue.dismiss() 55 | } 56 | } 57 | } 58 | #endif 59 | -------------------------------------------------------------------------------- /Sources/EditValueView/Editors/Image/SwiftUIImageEditor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftUIImageEditor.swift 3 | // 4 | // 5 | // Created by p-x9 on 2023/07/25. 6 | // 7 | // 8 | 9 | import SwiftUI 10 | 11 | #if canImport(UIKit) 12 | struct SwiftUIImageEditor: View { 13 | enum SourceType: Int, Identifiable { 14 | case library 15 | case camera 16 | 17 | var id: Int { rawValue } 18 | 19 | var type: UIImagePickerController.SourceType { 20 | switch self { 21 | case .library: return .photoLibrary 22 | case .camera: return .camera 23 | } 24 | } 25 | } 26 | 27 | @Binding var image: Image? 28 | @State var sourceTypeOfPicker: SourceType? 29 | @State var isPresentedActionSheet = false 30 | @State var isPresentedFileImporter = false 31 | 32 | /// A boolean value that indicates whether the type being edited is an Optional type or not 33 | var isOptional: Bool 34 | 35 | var body: some View { 36 | VStack { 37 | HStack { 38 | Text("tap to select image") 39 | .foregroundColor(.gray) 40 | .font(.footnote) 41 | Spacer() 42 | } 43 | 44 | editor 45 | .onTapGesture { 46 | isPresentedActionSheet = true 47 | } 48 | .onDrop(of: [.image], isTargeted: nil, perform: handleDropImage(providers:)) 49 | } 50 | .sheet(item: $sourceTypeOfPicker) { type in 51 | SwiftUIImagePicker(sourceType: type.type, selectedImage: $image) 52 | } 53 | .actionSheet(isPresented: $isPresentedActionSheet) { 54 | actionSheet 55 | } 56 | .fileImporter( 57 | isPresented: $isPresentedFileImporter, 58 | allowedContentTypes: [.image], 59 | allowsMultipleSelection: false 60 | ) { result in 61 | if case let .success(urls) = result, 62 | let url = urls.first, 63 | url.startAccessingSecurityScopedResource(), 64 | let nsuiImage = NSUIImage(contentsOfFile: url.path) { 65 | image = Image(uiImage: nsuiImage) 66 | } 67 | } 68 | } 69 | 70 | @ViewBuilder 71 | var editor: some View { 72 | if let image { 73 | image 74 | .renderingMode(.original) 75 | .resizable() 76 | .scaledToFit() 77 | .padding() 78 | .border(Color.iOS(.label), width: 0.5) 79 | } else { 80 | Color.iOS.secondarySystemFill 81 | .overlay( 82 | Text("current value is nil") 83 | .foregroundColor(.gray) 84 | ) 85 | } 86 | } 87 | 88 | var actionSheet: ActionSheet { 89 | var buttons: [ActionSheet.Button] = [ 90 | .default(Text("Photo Library")) { 91 | sourceTypeOfPicker = .library 92 | }, 93 | .default(Text("Camera")) { 94 | sourceTypeOfPicker = .camera 95 | }, 96 | .default(Text("File")) { 97 | isPresentedFileImporter = true 98 | }, 99 | .destructive(Text("Set nil")) { 100 | image = nil 101 | }, 102 | .cancel() 103 | ] 104 | 105 | if !isOptional { 106 | buttons.remove(at: buttons.count - 2) 107 | } 108 | 109 | return ActionSheet( 110 | title: Text("Select source"), 111 | buttons: buttons 112 | ) 113 | } 114 | } 115 | 116 | extension SwiftUIImageEditor { 117 | func handleDropImage(providers: [NSItemProvider]) -> Bool { 118 | guard let provider = providers.first else { return false } 119 | 120 | provider.loadObject(ofClass: NSUIImage.self) { item, error in 121 | guard let image = item as? NSUIImage else { return } 122 | DispatchQueue.main.async { 123 | self.image = Image(uiImage: image) 124 | } 125 | } 126 | 127 | return true 128 | } 129 | } 130 | 131 | #endif 132 | -------------------------------------------------------------------------------- /Sources/EditValueView/Extensions/CIImage+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CIImage+.swift 3 | // 4 | // 5 | // Created by p-x9 on 2023/07/28. 6 | // 7 | // 8 | 9 | #if canImport(UIKit) 10 | import UIKit 11 | 12 | extension CIImage { 13 | var uiImage: UIImage? { 14 | let context = CIContext() 15 | if let cgImage = context.createCGImage(self, from: self.extent) { 16 | return .init(cgImage: cgImage) 17 | } 18 | return UIImage(ciImage: self) 19 | } 20 | } 21 | #endif 22 | -------------------------------------------------------------------------------- /Sources/EditValueView/Extensions/Codable+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Codable+.swift 3 | // 4 | // 5 | // Created by p-x9 on 2022/10/22. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | extension Encodable { 12 | private static var jsonEncoder: JSONEncoder { 13 | let encoder = JSONEncoder() 14 | encoder.outputFormatting = [.prettyPrinted, .sortedKeys] 15 | return encoder 16 | } 17 | 18 | var jsonString: String? { 19 | guard let data = try? Self.jsonEncoder.encode(self) else { 20 | return nil 21 | } 22 | return String(data: data, encoding: .utf8) 23 | } 24 | } 25 | 26 | extension Decodable { 27 | private static var jsonDecoder: JSONDecoder { 28 | let decoder = JSONDecoder() 29 | return decoder 30 | } 31 | 32 | static func value(from jsonString: String) -> Self? { 33 | guard let data = jsonString.data(using: .utf8) else { 34 | return nil 35 | } 36 | return try? Self.jsonDecoder.decode(Self.self, from: data) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/EditValueView/Extensions/UIImage+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImage.swift 3 | // 4 | // 5 | // Created by p-x9 on 2023/07/28. 6 | // 7 | // 8 | 9 | #if canImport(UIKit) 10 | import UIKit 11 | 12 | extension UIImage { 13 | func toCIImage() -> CIImage? { 14 | if let ciImage { 15 | return ciImage 16 | } else if let cgImage { 17 | return CIImage(cgImage: cgImage) 18 | } 19 | return nil 20 | } 21 | } 22 | #endif 23 | -------------------------------------------------------------------------------- /Sources/EditValueView/Extensions/View+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View+.swift 3 | // 4 | // 5 | // Created by p-x9 on 2023/07/30. 6 | // 7 | // 8 | 9 | import SwiftUI 10 | 11 | extension View { 12 | func set(_ value: T, for keyPath: WritableKeyPath) -> Self { 13 | var new = self 14 | new[keyPath: keyPath] = value 15 | return new 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/EditValueView/Protocol/DefaultRepresentable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultRepresentable.swift 3 | // 4 | // 5 | // Created by p-x9 on 2022/10/17. 6 | // 7 | // 8 | import Foundation 9 | import CoreGraphics 10 | 11 | public protocol DefaultRepresentable { 12 | static var defaultValue: Self { get } 13 | } 14 | 15 | extension String: DefaultRepresentable { 16 | public static var defaultValue: String { 17 | "" 18 | } 19 | } 20 | extension Int: DefaultRepresentable { 21 | public static var defaultValue: Int { 22 | 0 23 | } 24 | } 25 | 26 | extension Float: DefaultRepresentable { 27 | public static var defaultValue: Float { 28 | 0.0 29 | } 30 | } 31 | 32 | extension Double: DefaultRepresentable { 33 | public static var defaultValue: Double { 34 | 0.0 35 | } 36 | } 37 | 38 | extension NSNumber: DefaultRepresentable { 39 | public static var defaultValue: Self { 40 | 0 41 | } 42 | } 43 | 44 | extension Bool: DefaultRepresentable { 45 | public static var defaultValue: Bool { 46 | false 47 | } 48 | } 49 | 50 | extension Date: DefaultRepresentable { 51 | public static var defaultValue: Date { 52 | .init() 53 | } 54 | } 55 | 56 | extension Data: DefaultRepresentable { 57 | public static var defaultValue: Data { 58 | .init() 59 | } 60 | } 61 | 62 | extension Array: DefaultRepresentable { 63 | public static var defaultValue: [Element] { 64 | [] 65 | } 66 | } 67 | 68 | extension Dictionary: DefaultRepresentable { 69 | public static var defaultValue: [Key: Value] { 70 | [:] 71 | } 72 | } 73 | 74 | extension CGFloat: DefaultRepresentable { 75 | public static var defaultValue: CGFloat { 76 | 0.0 77 | } 78 | } 79 | 80 | extension CGSize: DefaultRepresentable { 81 | public static var defaultValue: CGSize { 82 | .init(width: 0, height: 0) 83 | } 84 | } 85 | 86 | extension CGPoint: DefaultRepresentable { 87 | public static var defaultValue: CGPoint { 88 | .init(x: 0, y: 0) 89 | } 90 | } 91 | 92 | extension CGRect: DefaultRepresentable { 93 | public static var defaultValue: CGRect { 94 | .init(origin: .defaultValue, size: .defaultValue) 95 | } 96 | } 97 | 98 | extension CGColor: DefaultRepresentable { 99 | public static var defaultValue: Self { 100 | .init(red: 0, green: 0, blue: 0, alpha: 1) 101 | } 102 | } 103 | 104 | #if canImport(CoreImage) 105 | import CoreImage 106 | extension CIColor: DefaultRepresentable { 107 | public static var defaultValue: Self { 108 | .init() 109 | } 110 | } 111 | #endif 112 | 113 | #if canImport(SwiftUI) 114 | import SwiftUI 115 | extension Color: DefaultRepresentable { 116 | public static var defaultValue: Self { 117 | .black 118 | } 119 | } 120 | #endif 121 | 122 | extension NSUIColor: DefaultRepresentable { 123 | public static var defaultValue: Self { 124 | .init() 125 | } 126 | } 127 | 128 | extension Optional: DefaultRepresentable where Wrapped: DefaultRepresentable { 129 | public static var defaultValue: Self { 130 | Wrapped.defaultValue 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /Sources/EditValueView/Protocol/OptionalCaseIterable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OptionalCaseIterable.swift 3 | // 4 | // 5 | // Created by p-x9 on 2023/07/23. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | public protocol OptionalCaseIterable: OptionalType where Wrapped: CaseIterable { 12 | /// A type that can represent a collection of all values of this type. 13 | associatedtype OptionalAllCases: Collection = [Wrapped?] where Wrapped == Wrapped.AllCases.Element 14 | 15 | /// A collection of all values of this type. 16 | static var optionalAllCases: Self.OptionalAllCases { get } 17 | } 18 | 19 | extension OptionalCaseIterable { 20 | public static var optionalAllCases: [Wrapped?] { 21 | Array(Wrapped.allCases.map { .some($0) }) + [.none] 22 | } 23 | } 24 | 25 | extension Optional: OptionalCaseIterable where Wrapped: CaseIterable {} 26 | -------------------------------------------------------------------------------- /Sources/EditValueView/Protocol/OptionalNumeric.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OptionalNumeric.swift 3 | // 4 | // 5 | // Created by p-x9 on 2023/07/23. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | public protocol OptionalNumeric: OptionalType where Wrapped: Numeric {} 12 | 13 | extension Optional: OptionalNumeric where Wrapped: Numeric {} 14 | -------------------------------------------------------------------------------- /Sources/EditValueView/Protocol/OptionalType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OptionalType.swift 3 | // 4 | // 5 | // Created by p-x9 on 2023/07/23. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | public protocol OptionalType { 12 | associatedtype Wrapped 13 | 14 | var wrapped: Wrapped? { get } 15 | } 16 | 17 | extension OptionalType { 18 | static var isWrappedCaseiterable: Bool { 19 | Wrapped.self is any CaseIterable.Type 20 | } 21 | } 22 | 23 | extension Optional: OptionalType { 24 | public var wrapped: Wrapped? { self } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/EditValueView/UIKit/EditValueViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EditValueViewController.swift 3 | // 4 | // 5 | // Created by p-x9 on 2022/10/28. 6 | // 7 | // 8 | 9 | #if canImport(UIKit) 10 | import UIKit 11 | import SwiftUI 12 | 13 | public class EditValueViewController: UIHostingController> { 14 | 15 | /// Name of the property to be edited 16 | /// Used for navigation titles and type descriptions. 17 | let key: String 18 | 19 | /// This is called when editing is completed by pressing the save button. 20 | public var onUpdate: ((Value) -> Void)? 21 | 22 | /// Used to perform validation checks when editing values 23 | public var validate: ((Value) -> Bool)? 24 | 25 | /// Initialize with key and value 26 | /// - Parameters: 27 | /// - key: Name of the property to be edited. Used for navigation titles and type descriptions. 28 | /// - value: Initial value of the value to be edited 29 | public init( 30 | _ target: Root, 31 | key: String, 32 | keyPath: WritableKeyPath 33 | ) { 34 | self.key = key 35 | 36 | super.init(rootView: .init(target, key: key, keyPath: keyPath)) 37 | 38 | rootView = rootView 39 | .onUpdate({ newValue in 40 | self.onUpdate?(newValue) 41 | }) 42 | .validate({ newValue in 43 | self.validate?(newValue) ?? true 44 | }) 45 | } 46 | 47 | /// initialize with keyPath 48 | /// - Parameters: 49 | /// - target: Target object that has the property to be edited. 50 | /// - key: Name of the property to be edited. Used for navigation titles and type descriptions. 51 | /// - keyPath: keyPath of the property to be edited. 52 | public init(key: String, value: Value) { 53 | self.key = key 54 | 55 | super.init(rootView: .init(key: key, value: value)) 56 | 57 | rootView = rootView 58 | .onUpdate({ newValue in 59 | self.onUpdate?(newValue) 60 | }) 61 | .validate({ newValue in 62 | self.validate?(newValue) ?? true 63 | }) 64 | } 65 | 66 | @available(*, unavailable) 67 | required init?(coder: NSCoder) { 68 | fatalError("init(coder:) has not been implemented") 69 | } 70 | 71 | public override func viewDidLoad() { 72 | super.viewDidLoad() 73 | 74 | let style: EditValueView.PresentationStyle 75 | 76 | if navigationController == nil { 77 | style = .modal 78 | } else { 79 | style = .push 80 | } 81 | 82 | rootView = rootView 83 | .presentationStyle(style) 84 | } 85 | } 86 | 87 | #endif 88 | -------------------------------------------------------------------------------- /Sources/EditValueView/Util/Typealias.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Typealias.swift 3 | // 4 | // 5 | // Created by p-x9 on 2022/10/30. 6 | // 7 | // 8 | 9 | #if canImport(UIKit) 10 | import UIKit 11 | typealias NSUIColor = UIColor 12 | typealias NSUIImage = UIImage 13 | #endif 14 | 15 | #if canImport(Cocoa) 16 | import Cocoa 17 | typealias NSUIColor = NSColor 18 | typealias NSUIImage = NSImage 19 | #endif 20 | -------------------------------------------------------------------------------- /Tests/EditValueViewTests/EditValueViewTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import EditValueView 3 | 4 | final class EditValueViewTests: XCTestCase { 5 | } 6 | --------------------------------------------------------------------------------