├── .github └── workflows │ └── swift.yml ├── .gitignore ├── .swift-version ├── .swiftformat ├── .swiftlint.yml ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── contents.xcworkspacedata ├── Demo ├── SwiftFieldsDemo.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved └── SwiftFieldsDemo │ ├── AngleEditorDemo.swift │ ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json │ ├── ContentView.swift │ ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json │ ├── Support.swift │ ├── SwiftFieldsDemo.entitlements │ └── SwiftFieldsDemoApp.swift ├── Documentation ├── AngleEditorDemo.png ├── ClosedRangeSliderDemo.png ├── PathSliderDemo.png └── YASliderDemo.png ├── LICENSE ├── Package.resolved ├── Package.swift ├── README.md ├── Sources └── SwiftFields │ ├── AngleEditor.swift │ ├── ClosedRangeSlider.swift │ ├── PathSlider.swift │ ├── Support.swift │ └── YASlider.swift └── Tests └── SwiftFieldsTests └── SwiftFieldsTests.swift /.github/workflows/swift.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Swift project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-swift 3 | 4 | name: Swift 5 | 6 | on: 7 | push: 8 | pull_request: 9 | 10 | jobs: 11 | build: 12 | runs-on: macos-15 13 | steps: 14 | - uses: maxim-lobanov/setup-xcode@v1 15 | with: 16 | xcode-version: 16 17 | - uses: actions/checkout@v3 18 | - name: Build 19 | run: swift build -v 20 | - name: Run tests 21 | run: swift test -v 22 | - name: Build Demo 23 | run: cd Demo && xcodebuild -scheme SwiftFieldsDemo -destination 'platform=iOS Simulator,name=iPhone 16 Plus' build 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | # *.xcodeproj 44 | # 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | # .swiftpm 48 | 49 | .build/ 50 | 51 | # CocoaPods 52 | # 53 | # We recommend against adding the Pods directory to your .gitignore. However 54 | # you should judge for yourself, the pros and cons are mentioned at: 55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 56 | # 57 | # Pods/ 58 | # 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | # *.xcworkspace 61 | 62 | # Carthage 63 | # 64 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 65 | # Carthage/Checkouts 66 | 67 | Carthage/Build/ 68 | 69 | # Accio dependency management 70 | Dependencies/ 71 | .accio/ 72 | 73 | # fastlane 74 | # 75 | # It is recommended to not store the screenshots in the git repo. 76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 77 | # For more information about the recommended setup visit: 78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 79 | 80 | fastlane/report.xml 81 | fastlane/Preview.html 82 | fastlane/screenshots/**/*.png 83 | fastlane/test_output 84 | 85 | # Code Injection 86 | # 87 | # After new code Injection tools there's a generated folder /iOSInjectionProject 88 | # https://github.com/johnno1962/injectionforxcode 89 | 90 | iOSInjectionProject/ 91 | -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 5.8 2 | -------------------------------------------------------------------------------- /.swiftformat: -------------------------------------------------------------------------------- 1 | --disable andOperator 2 | --disable emptyBraces 3 | --disable fileHeader 4 | --disable redundantParens 5 | --disable trailingClosures 6 | --enable isEmpty 7 | 8 | --elseposition next-line 9 | --ifdef indent 10 | --patternlet inline 11 | --stripunusedargs closure-only 12 | --closingparen balanced 13 | --wraparguments preserve 14 | --wrapcollections before-first 15 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | analyzer_rules: 2 | - capture_variable 3 | - explicit_self 4 | - typesafe_array_init 5 | - unused_declaration 6 | - unused_import 7 | only_rules: 8 | - accessibility_label_for_image 9 | # - anonymous_argument_in_multiline_closure 10 | - array_init 11 | - balanced_xctest_lifecycle 12 | - block_based_kvo 13 | - class_delegate_protocol 14 | - closing_brace 15 | - closure_body_length 16 | - closure_end_indentation 17 | # - closure_parameter_position 18 | - closure_spacing 19 | - collection_alignment 20 | - colon 21 | - comma 22 | - comma_inheritance 23 | # - comment_spacing 24 | - compiler_protocol_init 25 | - computed_accessors_order 26 | - conditional_returns_on_newline 27 | - contains_over_filter_count 28 | - contains_over_filter_is_empty 29 | - contains_over_first_not_nil 30 | - contains_over_range_nil_comparison 31 | - control_statement 32 | - convenience_type 33 | - custom_rules 34 | - cyclomatic_complexity 35 | - deployment_target 36 | - discarded_notification_center_observer 37 | - discouraged_assert 38 | - discouraged_direct_init 39 | # - discouraged_none_name 40 | - discouraged_object_literal 41 | - discouraged_optional_boolean 42 | # - discouraged_optional_collection 43 | - duplicate_enum_cases 44 | - duplicate_imports 45 | - duplicated_key_in_dictionary_literal 46 | - dynamic_inline 47 | - empty_collection_literal 48 | - empty_count 49 | - empty_enum_arguments 50 | - empty_parameters 51 | # - empty_parentheses_with_trailing_closure 52 | - empty_string 53 | # - empty_xctest_method 54 | - enum_case_associated_values_count 55 | - expiring_todo 56 | # - explicit_acl 57 | - explicit_enum_raw_value 58 | - explicit_init 59 | # - explicit_top_level_acl 60 | # - explicit_type_interface 61 | - extension_access_modifier 62 | - fallthrough 63 | - fatal_error_message 64 | # - file_header 65 | - file_length 66 | # - file_name 67 | # - file_name_no_space 68 | # - file_types_order 69 | - first_where 70 | - flatmap_over_map_reduce 71 | - for_where 72 | # - force_cast 73 | # - force_try 74 | # - force_unwrapping 75 | - function_body_length 76 | # - function_default_parameter_at_end 77 | - function_parameter_count 78 | - generic_type_name 79 | - ibinspectable_in_extension 80 | - identical_operands 81 | # - identifier_name 82 | - implicit_getter 83 | # - implicit_return 84 | # - implicitly_unwrapped_optional 85 | - inclusive_language 86 | # - indentation_width 87 | - is_disjoint 88 | - joined_default_parameter 89 | # - large_tuple 90 | - last_where 91 | - leading_whitespace 92 | - legacy_cggeometry_functions 93 | - legacy_constant 94 | - legacy_constructor 95 | - legacy_hashing 96 | - legacy_multiple 97 | - legacy_nsgeometry_functions 98 | # - legacy_objc_type 99 | - legacy_random 100 | # - let_var_whitespace 101 | # - line_length 102 | - literal_expression_end_indentation 103 | - lower_acl_than_parent 104 | - mark 105 | # - missing_docs 106 | - modifier_order 107 | - multiline_arguments 108 | # - multiline_arguments_brackets 109 | - multiline_function_chains 110 | - multiline_literal_brackets 111 | - multiline_parameters 112 | # - multiline_parameters_brackets 113 | - multiple_closures_with_trailing_closure 114 | # - nesting 115 | - nimble_operator 116 | # - no_extension_access_modifier 117 | - no_fallthrough_only 118 | # - no_grouping_extension 119 | - no_space_in_method_call 120 | - notification_center_detachment 121 | - nslocalizedstring_key 122 | - nslocalizedstring_require_bundle 123 | - nsobject_prefer_isequal 124 | # - number_separator 125 | - object_literal 126 | - opening_brace 127 | - operator_usage_whitespace 128 | - operator_whitespace 129 | - optional_enum_case_matching 130 | # - orphaned_doc_comment 131 | - overridden_super_call 132 | - override_in_extension 133 | # - pattern_matching_keywords 134 | # - prefer_nimble 135 | # - prefer_self_in_static_references 136 | - prefer_self_type_over_type_of_self 137 | - prefer_zero_over_explicit_init 138 | # - prefixed_toplevel_constant 139 | - private_action 140 | - private_outlet 141 | - private_over_fileprivate 142 | - private_subject 143 | - private_unit_test 144 | - prohibited_interface_builder 145 | - prohibited_super_call 146 | - protocol_property_accessors_order 147 | - quick_discouraged_call 148 | - quick_discouraged_focused_test 149 | - quick_discouraged_pending_test 150 | - raw_value_for_camel_cased_codable_enum 151 | - reduce_boolean 152 | - reduce_into 153 | - redundant_discardable_let 154 | - redundant_nil_coalescing 155 | - redundant_objc_attribute 156 | - redundant_optional_initialization 157 | - redundant_set_access_control 158 | - redundant_string_enum_value 159 | - redundant_type_annotation 160 | - redundant_void_return 161 | # - required_deinit 162 | - required_enum_case 163 | - return_arrow_whitespace 164 | - return_value_from_void_function 165 | - self_binding 166 | - self_in_property_initialization 167 | - shorthand_operator 168 | # - single_test_class 169 | - sorted_first_last 170 | # - sorted_imports 171 | # - statement_position 172 | - static_operator 173 | - strict_fileprivate 174 | - strong_iboutlet 175 | # - superfluous_disable_command 176 | - switch_case_alignment 177 | - switch_case_on_newline 178 | - syntactic_sugar 179 | # - test_case_accessibility 180 | # - todo 181 | - toggle_bool 182 | # - trailing_closure 183 | # - trailing_comma 184 | # - trailing_newline 185 | - trailing_semicolon 186 | - trailing_whitespace 187 | - type_body_length 188 | # - type_contents_order 189 | # - type_name 190 | - unavailable_condition 191 | - unavailable_function 192 | - unneeded_break_in_switch 193 | - unneeded_parentheses_in_closure_argument 194 | - unowned_variable_capture 195 | - untyped_error_in_catch 196 | - unused_closure_parameter 197 | - unused_control_flow_label 198 | - unused_enumerated 199 | - unused_optional_binding 200 | - unused_setter_value 201 | - valid_ibinspectable 202 | - vertical_parameter_alignment 203 | - vertical_parameter_alignment_on_call 204 | - vertical_whitespace 205 | # - vertical_whitespace_between_cases 206 | - vertical_whitespace_closing_braces 207 | - vertical_whitespace_opening_braces 208 | - void_function_in_ternary 209 | - void_return 210 | - weak_delegate 211 | # - xct_specific_matcher 212 | - xctfail_message 213 | - yoda_condition 214 | included: 215 | - Sources 216 | #line_length: 240 217 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Demo/SwiftFieldsDemo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 4515C7EA2A0EC56900E56D8D /* SwiftFields in Frameworks */ = {isa = PBXBuildFile; productRef = 4515C7E92A0EC56900E56D8D /* SwiftFields */; }; 11 | 45297B9B2A16AD0300BA1BC2 /* Support.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45297B9A2A16AD0300BA1BC2 /* Support.swift */; }; 12 | 45E5129A2A0EC5190070D177 /* SwiftFieldsDemoApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45E512992A0EC5190070D177 /* SwiftFieldsDemoApp.swift */; }; 13 | 45E5129C2A0EC5190070D177 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45E5129B2A0EC5190070D177 /* ContentView.swift */; }; 14 | 45E5129E2A0EC5190070D177 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 45E5129D2A0EC5190070D177 /* Assets.xcassets */; }; 15 | 45E512A22A0EC5190070D177 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 45E512A12A0EC5190070D177 /* Preview Assets.xcassets */; }; 16 | /* End PBXBuildFile section */ 17 | 18 | /* Begin PBXFileReference section */ 19 | 4515C7E72A0EC56100E56D8D /* SwiftFields */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = SwiftFields; path = ..; sourceTree = ""; }; 20 | 45297B9A2A16AD0300BA1BC2 /* Support.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Support.swift; sourceTree = ""; }; 21 | 45E512962A0EC5190070D177 /* SwiftFieldsDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SwiftFieldsDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 22 | 45E512992A0EC5190070D177 /* SwiftFieldsDemoApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftFieldsDemoApp.swift; sourceTree = ""; }; 23 | 45E5129B2A0EC5190070D177 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 24 | 45E5129D2A0EC5190070D177 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 25 | 45E5129F2A0EC5190070D177 /* SwiftFieldsDemo.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SwiftFieldsDemo.entitlements; sourceTree = ""; }; 26 | 45E512A12A0EC5190070D177 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 27 | /* End PBXFileReference section */ 28 | 29 | /* Begin PBXFrameworksBuildPhase section */ 30 | 45E512932A0EC5190070D177 /* Frameworks */ = { 31 | isa = PBXFrameworksBuildPhase; 32 | buildActionMask = 2147483647; 33 | files = ( 34 | 4515C7EA2A0EC56900E56D8D /* SwiftFields in Frameworks */, 35 | ); 36 | runOnlyForDeploymentPostprocessing = 0; 37 | }; 38 | /* End PBXFrameworksBuildPhase section */ 39 | 40 | /* Begin PBXGroup section */ 41 | 4515C7E62A0EC56100E56D8D /* Packages */ = { 42 | isa = PBXGroup; 43 | children = ( 44 | 4515C7E72A0EC56100E56D8D /* SwiftFields */, 45 | ); 46 | name = Packages; 47 | sourceTree = ""; 48 | }; 49 | 4515C7E82A0EC56900E56D8D /* Frameworks */ = { 50 | isa = PBXGroup; 51 | children = ( 52 | ); 53 | name = Frameworks; 54 | sourceTree = ""; 55 | }; 56 | 45E5128D2A0EC5180070D177 = { 57 | isa = PBXGroup; 58 | children = ( 59 | 4515C7E62A0EC56100E56D8D /* Packages */, 60 | 45E512982A0EC5190070D177 /* SwiftFieldsDemo */, 61 | 45E512972A0EC5190070D177 /* Products */, 62 | 4515C7E82A0EC56900E56D8D /* Frameworks */, 63 | ); 64 | sourceTree = ""; 65 | }; 66 | 45E512972A0EC5190070D177 /* Products */ = { 67 | isa = PBXGroup; 68 | children = ( 69 | 45E512962A0EC5190070D177 /* SwiftFieldsDemo.app */, 70 | ); 71 | name = Products; 72 | sourceTree = ""; 73 | }; 74 | 45E512982A0EC5190070D177 /* SwiftFieldsDemo */ = { 75 | isa = PBXGroup; 76 | children = ( 77 | 45E512992A0EC5190070D177 /* SwiftFieldsDemoApp.swift */, 78 | 45E5129B2A0EC5190070D177 /* ContentView.swift */, 79 | 45297B9A2A16AD0300BA1BC2 /* Support.swift */, 80 | 45E5129D2A0EC5190070D177 /* Assets.xcassets */, 81 | 45E5129F2A0EC5190070D177 /* SwiftFieldsDemo.entitlements */, 82 | 45E512A02A0EC5190070D177 /* Preview Content */, 83 | ); 84 | path = SwiftFieldsDemo; 85 | sourceTree = ""; 86 | }; 87 | 45E512A02A0EC5190070D177 /* Preview Content */ = { 88 | isa = PBXGroup; 89 | children = ( 90 | 45E512A12A0EC5190070D177 /* Preview Assets.xcassets */, 91 | ); 92 | path = "Preview Content"; 93 | sourceTree = ""; 94 | }; 95 | /* End PBXGroup section */ 96 | 97 | /* Begin PBXNativeTarget section */ 98 | 45E512952A0EC5190070D177 /* SwiftFieldsDemo */ = { 99 | isa = PBXNativeTarget; 100 | buildConfigurationList = 45E512A52A0EC5190070D177 /* Build configuration list for PBXNativeTarget "SwiftFieldsDemo" */; 101 | buildPhases = ( 102 | 45E512922A0EC5190070D177 /* Sources */, 103 | 45E512932A0EC5190070D177 /* Frameworks */, 104 | 45E512942A0EC5190070D177 /* Resources */, 105 | ); 106 | buildRules = ( 107 | ); 108 | dependencies = ( 109 | ); 110 | name = SwiftFieldsDemo; 111 | packageProductDependencies = ( 112 | 4515C7E92A0EC56900E56D8D /* SwiftFields */, 113 | ); 114 | productName = SwiftFieldsDemo; 115 | productReference = 45E512962A0EC5190070D177 /* SwiftFieldsDemo.app */; 116 | productType = "com.apple.product-type.application"; 117 | }; 118 | /* End PBXNativeTarget section */ 119 | 120 | /* Begin PBXProject section */ 121 | 45E5128E2A0EC5180070D177 /* Project object */ = { 122 | isa = PBXProject; 123 | attributes = { 124 | BuildIndependentTargetsInParallel = 1; 125 | LastSwiftUpdateCheck = 1430; 126 | LastUpgradeCheck = 1500; 127 | TargetAttributes = { 128 | 45E512952A0EC5190070D177 = { 129 | CreatedOnToolsVersion = 14.3; 130 | }; 131 | }; 132 | }; 133 | buildConfigurationList = 45E512912A0EC5180070D177 /* Build configuration list for PBXProject "SwiftFieldsDemo" */; 134 | compatibilityVersion = "Xcode 14.0"; 135 | developmentRegion = en; 136 | hasScannedForEncodings = 0; 137 | knownRegions = ( 138 | en, 139 | Base, 140 | ); 141 | mainGroup = 45E5128D2A0EC5180070D177; 142 | productRefGroup = 45E512972A0EC5190070D177 /* Products */; 143 | projectDirPath = ""; 144 | projectRoot = ""; 145 | targets = ( 146 | 45E512952A0EC5190070D177 /* SwiftFieldsDemo */, 147 | ); 148 | }; 149 | /* End PBXProject section */ 150 | 151 | /* Begin PBXResourcesBuildPhase section */ 152 | 45E512942A0EC5190070D177 /* Resources */ = { 153 | isa = PBXResourcesBuildPhase; 154 | buildActionMask = 2147483647; 155 | files = ( 156 | 45E512A22A0EC5190070D177 /* Preview Assets.xcassets in Resources */, 157 | 45E5129E2A0EC5190070D177 /* Assets.xcassets in Resources */, 158 | ); 159 | runOnlyForDeploymentPostprocessing = 0; 160 | }; 161 | /* End PBXResourcesBuildPhase section */ 162 | 163 | /* Begin PBXSourcesBuildPhase section */ 164 | 45E512922A0EC5190070D177 /* Sources */ = { 165 | isa = PBXSourcesBuildPhase; 166 | buildActionMask = 2147483647; 167 | files = ( 168 | 45297B9B2A16AD0300BA1BC2 /* Support.swift in Sources */, 169 | 45E5129C2A0EC5190070D177 /* ContentView.swift in Sources */, 170 | 45E5129A2A0EC5190070D177 /* SwiftFieldsDemoApp.swift in Sources */, 171 | ); 172 | runOnlyForDeploymentPostprocessing = 0; 173 | }; 174 | /* End PBXSourcesBuildPhase section */ 175 | 176 | /* Begin XCBuildConfiguration section */ 177 | 45E512A32A0EC5190070D177 /* Debug */ = { 178 | isa = XCBuildConfiguration; 179 | buildSettings = { 180 | ALWAYS_SEARCH_USER_PATHS = NO; 181 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 182 | CLANG_ANALYZER_NONNULL = YES; 183 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 184 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 185 | CLANG_ENABLE_MODULES = YES; 186 | CLANG_ENABLE_OBJC_ARC = YES; 187 | CLANG_ENABLE_OBJC_WEAK = YES; 188 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 189 | CLANG_WARN_BOOL_CONVERSION = YES; 190 | CLANG_WARN_COMMA = YES; 191 | CLANG_WARN_CONSTANT_CONVERSION = YES; 192 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 193 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 194 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 195 | CLANG_WARN_EMPTY_BODY = YES; 196 | CLANG_WARN_ENUM_CONVERSION = YES; 197 | CLANG_WARN_INFINITE_RECURSION = YES; 198 | CLANG_WARN_INT_CONVERSION = YES; 199 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 200 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 201 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 202 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 203 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 204 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 205 | CLANG_WARN_STRICT_PROTOTYPES = YES; 206 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 207 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 208 | CLANG_WARN_UNREACHABLE_CODE = YES; 209 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 210 | COPY_PHASE_STRIP = NO; 211 | DEAD_CODE_STRIPPING = YES; 212 | DEBUG_INFORMATION_FORMAT = dwarf; 213 | ENABLE_STRICT_OBJC_MSGSEND = YES; 214 | ENABLE_TESTABILITY = YES; 215 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 216 | GCC_C_LANGUAGE_STANDARD = gnu11; 217 | GCC_DYNAMIC_NO_PIC = NO; 218 | GCC_NO_COMMON_BLOCKS = YES; 219 | GCC_OPTIMIZATION_LEVEL = 0; 220 | GCC_PREPROCESSOR_DEFINITIONS = ( 221 | "DEBUG=1", 222 | "$(inherited)", 223 | ); 224 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 225 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 226 | GCC_WARN_UNDECLARED_SELECTOR = YES; 227 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 228 | GCC_WARN_UNUSED_FUNCTION = YES; 229 | GCC_WARN_UNUSED_VARIABLE = YES; 230 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 231 | MTL_FAST_MATH = YES; 232 | ONLY_ACTIVE_ARCH = YES; 233 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 234 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 235 | }; 236 | name = Debug; 237 | }; 238 | 45E512A42A0EC5190070D177 /* Release */ = { 239 | isa = XCBuildConfiguration; 240 | buildSettings = { 241 | ALWAYS_SEARCH_USER_PATHS = NO; 242 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 243 | CLANG_ANALYZER_NONNULL = YES; 244 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 245 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 246 | CLANG_ENABLE_MODULES = YES; 247 | CLANG_ENABLE_OBJC_ARC = YES; 248 | CLANG_ENABLE_OBJC_WEAK = YES; 249 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 250 | CLANG_WARN_BOOL_CONVERSION = YES; 251 | CLANG_WARN_COMMA = YES; 252 | CLANG_WARN_CONSTANT_CONVERSION = YES; 253 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 254 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 255 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 256 | CLANG_WARN_EMPTY_BODY = YES; 257 | CLANG_WARN_ENUM_CONVERSION = YES; 258 | CLANG_WARN_INFINITE_RECURSION = YES; 259 | CLANG_WARN_INT_CONVERSION = YES; 260 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 261 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 262 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 263 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 264 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 265 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 266 | CLANG_WARN_STRICT_PROTOTYPES = YES; 267 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 268 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 269 | CLANG_WARN_UNREACHABLE_CODE = YES; 270 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 271 | COPY_PHASE_STRIP = NO; 272 | DEAD_CODE_STRIPPING = YES; 273 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 274 | ENABLE_NS_ASSERTIONS = NO; 275 | ENABLE_STRICT_OBJC_MSGSEND = YES; 276 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 277 | GCC_C_LANGUAGE_STANDARD = gnu11; 278 | GCC_NO_COMMON_BLOCKS = YES; 279 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 280 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 281 | GCC_WARN_UNDECLARED_SELECTOR = YES; 282 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 283 | GCC_WARN_UNUSED_FUNCTION = YES; 284 | GCC_WARN_UNUSED_VARIABLE = YES; 285 | MTL_ENABLE_DEBUG_INFO = NO; 286 | MTL_FAST_MATH = YES; 287 | SWIFT_COMPILATION_MODE = wholemodule; 288 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 289 | }; 290 | name = Release; 291 | }; 292 | 45E512A62A0EC5190070D177 /* Debug */ = { 293 | isa = XCBuildConfiguration; 294 | buildSettings = { 295 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 296 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 297 | CODE_SIGN_ENTITLEMENTS = SwiftFieldsDemo/SwiftFieldsDemo.entitlements; 298 | CODE_SIGN_STYLE = Automatic; 299 | CURRENT_PROJECT_VERSION = 1; 300 | DEAD_CODE_STRIPPING = YES; 301 | DEVELOPMENT_ASSET_PATHS = "\"SwiftFieldsDemo/Preview Content\""; 302 | DEVELOPMENT_TEAM = 6E23EP94PG; 303 | ENABLE_HARDENED_RUNTIME = YES; 304 | ENABLE_PREVIEWS = YES; 305 | GENERATE_INFOPLIST_FILE = YES; 306 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; 307 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; 308 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; 309 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; 310 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; 311 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; 312 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; 313 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; 314 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 315 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 316 | IPHONEOS_DEPLOYMENT_TARGET = 16.0; 317 | LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; 318 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; 319 | MACOSX_DEPLOYMENT_TARGET = 13.0; 320 | MARKETING_VERSION = 1.0; 321 | PRODUCT_BUNDLE_IDENTIFIER = io.schwa.SwiftFieldsDemo; 322 | PRODUCT_NAME = "$(TARGET_NAME)"; 323 | SDKROOT = auto; 324 | SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator macosx"; 325 | SUPPORTS_MACCATALYST = NO; 326 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; 327 | SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; 328 | SWIFT_EMIT_LOC_STRINGS = YES; 329 | SWIFT_VERSION = 5.0; 330 | TARGETED_DEVICE_FAMILY = "1,2,3"; 331 | TVOS_DEPLOYMENT_TARGET = 16.0; 332 | }; 333 | name = Debug; 334 | }; 335 | 45E512A72A0EC5190070D177 /* Release */ = { 336 | isa = XCBuildConfiguration; 337 | buildSettings = { 338 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 339 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 340 | CODE_SIGN_ENTITLEMENTS = SwiftFieldsDemo/SwiftFieldsDemo.entitlements; 341 | CODE_SIGN_STYLE = Automatic; 342 | CURRENT_PROJECT_VERSION = 1; 343 | DEAD_CODE_STRIPPING = YES; 344 | DEVELOPMENT_ASSET_PATHS = "\"SwiftFieldsDemo/Preview Content\""; 345 | DEVELOPMENT_TEAM = 6E23EP94PG; 346 | ENABLE_HARDENED_RUNTIME = YES; 347 | ENABLE_PREVIEWS = YES; 348 | GENERATE_INFOPLIST_FILE = YES; 349 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; 350 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; 351 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; 352 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; 353 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; 354 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; 355 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; 356 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; 357 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 358 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 359 | IPHONEOS_DEPLOYMENT_TARGET = 16.0; 360 | LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; 361 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; 362 | MACOSX_DEPLOYMENT_TARGET = 13.0; 363 | MARKETING_VERSION = 1.0; 364 | PRODUCT_BUNDLE_IDENTIFIER = io.schwa.SwiftFieldsDemo; 365 | PRODUCT_NAME = "$(TARGET_NAME)"; 366 | SDKROOT = auto; 367 | SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator macosx"; 368 | SUPPORTS_MACCATALYST = NO; 369 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; 370 | SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; 371 | SWIFT_EMIT_LOC_STRINGS = YES; 372 | SWIFT_VERSION = 5.0; 373 | TARGETED_DEVICE_FAMILY = "1,2,3"; 374 | TVOS_DEPLOYMENT_TARGET = 16.0; 375 | }; 376 | name = Release; 377 | }; 378 | /* End XCBuildConfiguration section */ 379 | 380 | /* Begin XCConfigurationList section */ 381 | 45E512912A0EC5180070D177 /* Build configuration list for PBXProject "SwiftFieldsDemo" */ = { 382 | isa = XCConfigurationList; 383 | buildConfigurations = ( 384 | 45E512A32A0EC5190070D177 /* Debug */, 385 | 45E512A42A0EC5190070D177 /* Release */, 386 | ); 387 | defaultConfigurationIsVisible = 0; 388 | defaultConfigurationName = Release; 389 | }; 390 | 45E512A52A0EC5190070D177 /* Build configuration list for PBXNativeTarget "SwiftFieldsDemo" */ = { 391 | isa = XCConfigurationList; 392 | buildConfigurations = ( 393 | 45E512A62A0EC5190070D177 /* Debug */, 394 | 45E512A72A0EC5190070D177 /* Release */, 395 | ); 396 | defaultConfigurationIsVisible = 0; 397 | defaultConfigurationName = Release; 398 | }; 399 | /* End XCConfigurationList section */ 400 | 401 | /* Begin XCSwiftPackageProductDependency section */ 402 | 4515C7E92A0EC56900E56D8D /* SwiftFields */ = { 403 | isa = XCSwiftPackageProductDependency; 404 | productName = SwiftFields; 405 | }; 406 | /* End XCSwiftPackageProductDependency section */ 407 | }; 408 | rootObject = 45E5128E2A0EC5180070D177 /* Project object */; 409 | } 410 | -------------------------------------------------------------------------------- /Demo/SwiftFieldsDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Demo/SwiftFieldsDemo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Demo/SwiftFieldsDemo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "swift-algorithms", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/apple/swift-algorithms", 7 | "state" : { 8 | "revision" : "b14b7f4c528c942f121c8b860b9410b2bf57825e", 9 | "version" : "1.0.0" 10 | } 11 | }, 12 | { 13 | "identity" : "swift-numerics", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/apple/swift-numerics", 16 | "state" : { 17 | "revision" : "0a5bc04095a675662cf24757cc0640aa2204253b", 18 | "version" : "1.0.2" 19 | } 20 | }, 21 | { 22 | "identity" : "swiftformats", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/schwa/SwiftFormats", 25 | "state" : { 26 | "revision" : "de0d13b019b9a0b68b1d6a83e1f48db3c4d06512", 27 | "version" : "0.3.3" 28 | } 29 | } 30 | ], 31 | "version" : 2 32 | } 33 | -------------------------------------------------------------------------------- /Demo/SwiftFieldsDemo/AngleEditorDemo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AngleEditorDemo.swift 3 | // SwiftFieldsDemo 4 | // 5 | // Created by Jonathan Wight on 5/16/23. 6 | // 7 | 8 | import SwiftFields 9 | import SwiftUI 10 | import SwiftFormats 11 | 12 | -------------------------------------------------------------------------------- /Demo/SwiftFieldsDemo/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 | -------------------------------------------------------------------------------- /Demo/SwiftFieldsDemo/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 | -------------------------------------------------------------------------------- /Demo/SwiftFieldsDemo/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Demo/SwiftFieldsDemo/ContentView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import SwiftFields 3 | import SwiftFormats 4 | 5 | struct ContentView: View { 6 | var body: some View { 7 | NavigationView { 8 | List { 9 | #if !os(tvOS) 10 | NavigationLink("AngleEditorDemo") { 11 | AngleEditorDemo() 12 | } 13 | NavigationLink("ClosedRangeSliderDemo") { 14 | ClosedRangeSliderDemo() 15 | } 16 | NavigationLink("PathSliderDemo") { 17 | PathSliderDemo() 18 | } 19 | NavigationLink("YASliderDemo") { 20 | YASliderDemo() 21 | } 22 | #endif 23 | } 24 | .frame(minWidth: 200) 25 | } 26 | } 27 | } 28 | 29 | // MARK: - 30 | 31 | #if !os(tvOS) 32 | struct AngleEditorDemo: View { 33 | 34 | @State 35 | var limit = Angle(degrees: 0) ... Angle(degrees: 360) 36 | 37 | 38 | @State 39 | var value = Angle(degrees: 90) 40 | 41 | var body: some View { 42 | VStack { 43 | VStack { 44 | TextField("Limit", value: $limit, format: ClosedRangeFormatStyle(substyle: .angle)) 45 | } 46 | .padding() 47 | Spacer() 48 | AngleEditor(angle: $value, limit: limit) 49 | Spacer() 50 | } 51 | .frame(maxWidth: 160) 52 | } 53 | } 54 | 55 | // MARK: - 56 | 57 | struct ClosedRangeSliderDemo: View { 58 | 59 | @State 60 | var value = 0.0 ... 1.0 61 | 62 | var body: some View { 63 | VStack { 64 | VStack { 65 | TextField("Value", value: $value, format: ClosedRangeFormatStyle(substyle: .number)) 66 | Slider(value: $value.editableLowerBound, in: 0 ... value.upperBound, label: { Text("Lower bound")}) 67 | Slider(value: $value.editableUpperBound, in: value.lowerBound ... 1, label: { Text("Upper bound")}) 68 | } 69 | .padding() 70 | Spacer() 71 | ClosedRangeSlider(value: $value) 72 | Spacer() 73 | } 74 | .frame(width: 150) 75 | } 76 | } 77 | 78 | // MARK: - 79 | 80 | struct PathSliderDemo: View { 81 | 82 | @State 83 | var value: Double = 0 84 | 85 | enum Shape: CaseIterable { 86 | case line 87 | case wigglyLine 88 | case circle 89 | case roundedRect 90 | case star 91 | case logo 92 | } 93 | 94 | @State 95 | var shape = Shape.logo 96 | 97 | var path: Path { 98 | return shape.path 99 | } 100 | 101 | var body: some View { 102 | let frame = path.boundingRect.insetBy(dx: -10, dy: -10) 103 | VStack { 104 | VStack { 105 | Picker("Shape", selection: $shape) { 106 | ForEach(Shape.allCases, id: \.self) { shape in 107 | Text("\(String(describing: shape))").tag(shape) 108 | } 109 | } 110 | .labelsHidden() 111 | TextField("Value", value: $value, format: .number) 112 | Slider(value: $value, in: 0 ... 100) 113 | } 114 | .frame(maxWidth: 160) 115 | .padding() 116 | Spacer() 117 | PathSlider(value: $value, in: 0 ... 100, path: path.offsetBy(dx: 10, dy: 10)) 118 | .frame(width: frame.width, height: frame.height) 119 | .border(Color.pink.opacity(0.1)) 120 | Spacer() 121 | } 122 | } 123 | } 124 | 125 | extension PathSliderDemo.Shape { 126 | var path: Path { 127 | switch self { 128 | case .line: 129 | return Path { path in 130 | path.addLines([CGPoint(x: 0, y: 0), CGPoint(x: 100, y: 0)]) 131 | } 132 | case .wigglyLine: 133 | return Path { path in 134 | path.move(to: CGPoint.zero) 135 | path.addQuadCurve(to: CGPoint(x: 100, y: 50), control: CGPoint(x: 50, y: 100)) 136 | path.addQuadCurve(to: CGPoint(x: 200, y: 50), control: CGPoint(x: 150, y: 0)) 137 | } 138 | case .circle: 139 | return Path(ellipseIn: CGRect(x: 0, y: 0, width: 50, height: 50)) 140 | case .roundedRect: 141 | return Path(roundedRect: CGRect(x: 0, y: 0, width: 50, height: 50), cornerRadius: 8) 142 | case .star: 143 | return Path { path in 144 | path.addLines( 145 | [CGPoint(x: 0.5, y: 0), CGPoint(x: 0.618, y: 0.338), CGPoint(x: 0.976, y: 0.345), CGPoint(x: 0.69, y: 0.562), CGPoint(x: 0.794, y: 0.905), CGPoint(x: 0.5, y: 0.7), CGPoint(x: 0.206, y: 0.905), CGPoint(x: 0.31, y: 0.562), CGPoint(x: 0.024, y: 0.345), CGPoint(x: 0.382, y: 0.338)].map { CGPoint(x: $0.x * 100, y: $0.y * 100)} 146 | ) 147 | path.closeSubpath() 148 | } 149 | case .logo: 150 | return Path { path in 151 | // Apple 152 | path.move(to: CGPoint(x: 110.89, y: 99.2)) 153 | path.addCurve(to: CGPoint(x: 105.97, y: 108.09), control1: CGPoint(x: 109.5, y: 102.41), control2: CGPoint(x: 107.87, y: 105.37)) 154 | path.addCurve(to: CGPoint(x: 99.64, y: 115.79), control1: CGPoint(x: 103.39, y: 111.8), control2: CGPoint(x: 101.27, y: 114.37)) 155 | path.addCurve(to: CGPoint(x: 91.5, y: 119.4), control1: CGPoint(x: 97.11, y: 118.13), control2: CGPoint(x: 94.4, y: 119.33)) 156 | path.addCurve(to: CGPoint(x: 83.99, y: 117.59), control1: CGPoint(x: 89.42, y: 119.4), control2: CGPoint(x: 86.91, y: 118.8)) 157 | path.addCurve(to: CGPoint(x: 75.9, y: 115.79), control1: CGPoint(x: 81.06, y: 116.39), control2: CGPoint(x: 78.36, y: 115.79)) 158 | path.addCurve(to: CGPoint(x: 67.58, y: 117.59), control1: CGPoint(x: 73.31, y: 115.79), control2: CGPoint(x: 70.54, y: 116.39)) 159 | path.addCurve(to: CGPoint(x: 60.39, y: 119.49), control1: CGPoint(x: 64.61, y: 118.8), control2: CGPoint(x: 62.21, y: 119.43)) 160 | path.addCurve(to: CGPoint(x: 52.07, y: 115.79), control1: CGPoint(x: 57.6, y: 119.61), control2: CGPoint(x: 54.83, y: 118.38)) 161 | path.addCurve(to: CGPoint(x: 45.44, y: 107.82), control1: CGPoint(x: 50.3, y: 114.24), control2: CGPoint(x: 48.09, y: 111.58)) 162 | path.addCurve(to: CGPoint(x: 38.44, y: 93.82), control1: CGPoint(x: 42.6, y: 103.8), control2: CGPoint(x: 40.27, y: 99.14)) 163 | path.addCurve(to: CGPoint(x: 35.5, y: 77.15), control1: CGPoint(x: 36.48, y: 88.09), control2: CGPoint(x: 35.5, y: 82.53)) 164 | path.addCurve(to: CGPoint(x: 39.48, y: 61.21), control1: CGPoint(x: 35.5, y: 70.98), control2: CGPoint(x: 36.82, y: 65.67)) 165 | path.addCurve(to: CGPoint(x: 47.8, y: 52.74), control1: CGPoint(x: 41.56, y: 57.63), control2: CGPoint(x: 44.33, y: 54.81)) 166 | path.addCurve(to: CGPoint(x: 59.06, y: 49.54), control1: CGPoint(x: 51.27, y: 50.67), control2: CGPoint(x: 55.02, y: 49.61)) 167 | path.addCurve(to: CGPoint(x: 67.76, y: 51.58), control1: CGPoint(x: 61.27, y: 49.54), control2: CGPoint(x: 64.16, y: 50.23)) 168 | path.addCurve(to: CGPoint(x: 74.67, y: 53.62), control1: CGPoint(x: 71.35, y: 52.94), control2: CGPoint(x: 73.66, y: 53.62)) 169 | path.addCurve(to: CGPoint(x: 82.33, y: 51.22), control1: CGPoint(x: 75.42, y: 53.62), control2: CGPoint(x: 77.98, y: 52.82)) 170 | path.addCurve(to: CGPoint(x: 92.73, y: 49.36), control1: CGPoint(x: 86.43, y: 49.73), control2: CGPoint(x: 89.9, y: 49.12)) 171 | path.addCurve(to: CGPoint(x: 110.05, y: 58.53), control1: CGPoint(x: 100.43, y: 49.98), control2: CGPoint(x: 106.2, y: 53.03)) 172 | path.addCurve(to: CGPoint(x: 99.83, y: 76.13), control1: CGPoint(x: 103.17, y: 62.72), control2: CGPoint(x: 99.77, y: 68.59)) 173 | path.addCurve(to: CGPoint(x: 106.17, y: 90.76), control1: CGPoint(x: 99.89, y: 82), control2: CGPoint(x: 102.01, y: 86.88)) 174 | path.addCurve(to: CGPoint(x: 112.5, y: 94.94), control1: CGPoint(x: 108.05, y: 92.56), control2: CGPoint(x: 110.16, y: 93.95)) 175 | path.addCurve(to: CGPoint(x: 110.89, y: 99.2), control1: CGPoint(x: 111.99, y: 96.42), control2: CGPoint(x: 111.46, y: 97.84)) 176 | 177 | // Leaf 178 | path.move(to: CGPoint(x: 93.25, y: 29.36)) 179 | path.addCurve(to: CGPoint(x: 88.25, y: 42.23), control1: CGPoint(x: 93.25, y: 33.96), control2: CGPoint(x: 91.58, y: 38.26)) 180 | path.addCurve(to: CGPoint(x: 74.1, y: 49.26), control1: CGPoint(x: 84.23, y: 46.96), control2: CGPoint(x: 79.37, y: 49.69)) 181 | path.addCurve(to: CGPoint(x: 74, y: 47.52), control1: CGPoint(x: 74.03, y: 48.71), control2: CGPoint(x: 74, y: 48.13)) 182 | path.addCurve(to: CGPoint(x: 79.3, y: 34.51), control1: CGPoint(x: 74, y: 43.1), control2: CGPoint(x: 75.91, y: 38.38)) 183 | path.addCurve(to: CGPoint(x: 85.76, y: 29.63), control1: CGPoint(x: 80.99, y: 32.55), control2: CGPoint(x: 83.15, y: 30.93)) 184 | path.addCurve(to: CGPoint(x: 93.15, y: 27.52), control1: CGPoint(x: 88.37, y: 28.35), control2: CGPoint(x: 90.83, y: 27.65)) 185 | path.addCurve(to: CGPoint(x: 93.25, y: 29.36), control1: CGPoint(x: 93.22, y: 28.14), control2: CGPoint(x: 93.25, y: 28.75)) 186 | path.addLine(to: CGPoint(x: 93.25, y: 29.36)) 187 | 188 | path.closeSubpath() 189 | } 190 | .applying(.init(translationX: -35.5, y: -29.36)) 191 | 192 | } 193 | 194 | } 195 | } 196 | 197 | // MARK: - 198 | 199 | struct YASliderDemo: View { 200 | @State 201 | var value: Double = 50 202 | 203 | var body: some View { 204 | VStack { 205 | VStack { 206 | TextField("Value", value: $value, format: .number) 207 | Slider(value: $value, in: 0 ... 80) 208 | } 209 | .padding() 210 | Spacer() 211 | HStack { 212 | YASlider(value: $value, in: 0 ... 80, axis: .horizontal) 213 | YASlider(value: $value, in: 0 ... 80, axis: .vertical).frame(height: 120) 214 | } 215 | Spacer() 216 | } 217 | .frame(width: 100) 218 | } 219 | } 220 | #endif 221 | -------------------------------------------------------------------------------- /Demo/SwiftFieldsDemo/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Demo/SwiftFieldsDemo/Support.swift: -------------------------------------------------------------------------------- 1 | extension ClosedRange { 2 | var editableLowerBound: Bound { 3 | get { 4 | lowerBound 5 | } 6 | set { 7 | self = newValue ... upperBound 8 | } 9 | } 10 | var editableUpperBound: Bound { 11 | get { 12 | upperBound 13 | } 14 | set { 15 | self = lowerBound ... newValue 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Demo/SwiftFieldsDemo/SwiftFieldsDemo.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 | -------------------------------------------------------------------------------- /Demo/SwiftFieldsDemo/SwiftFieldsDemoApp.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | @main 4 | struct SwiftFieldsDemoApp: App { 5 | var body: some Scene { 6 | WindowGroup { 7 | ContentView() 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Documentation/AngleEditorDemo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schwa/SwiftFields/0cbdfba20cae4a16cafcd0b43216114fe861329c/Documentation/AngleEditorDemo.png -------------------------------------------------------------------------------- /Documentation/ClosedRangeSliderDemo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schwa/SwiftFields/0cbdfba20cae4a16cafcd0b43216114fe861329c/Documentation/ClosedRangeSliderDemo.png -------------------------------------------------------------------------------- /Documentation/PathSliderDemo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schwa/SwiftFields/0cbdfba20cae4a16cafcd0b43216114fe861329c/Documentation/PathSliderDemo.png -------------------------------------------------------------------------------- /Documentation/YASliderDemo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schwa/SwiftFields/0cbdfba20cae4a16cafcd0b43216114fe861329c/Documentation/YASliderDemo.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2023, Jonathan Wight 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "swift-algorithms", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/apple/swift-algorithms", 7 | "state" : { 8 | "revision" : "b14b7f4c528c942f121c8b860b9410b2bf57825e", 9 | "version" : "1.0.0" 10 | } 11 | }, 12 | { 13 | "identity" : "swift-numerics", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/apple/swift-numerics", 16 | "state" : { 17 | "revision" : "0a5bc04095a675662cf24757cc0640aa2204253b", 18 | "version" : "1.0.2" 19 | } 20 | }, 21 | { 22 | "identity" : "swiftformats", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/schwa/SwiftFormats", 25 | "state" : { 26 | "revision" : "758b0d73bc8f58985f68023ef75bc63b1f679bd5", 27 | "version" : "0.3.1" 28 | } 29 | } 30 | ], 31 | "version" : 2 32 | } 33 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.8 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "SwiftFields", 8 | platforms: [ 9 | .iOS(.v16), 10 | .macOS(.v13), 11 | .tvOS(.v16), 12 | ], 13 | products: [ 14 | .library( 15 | name: "SwiftFields", 16 | targets: ["SwiftFields"]), 17 | ], 18 | dependencies: [ 19 | .package(url: "https://github.com/schwa/SwiftFormats", from: "0.3.1") 20 | ], 21 | targets: [ 22 | .target( 23 | name: "SwiftFields", 24 | dependencies: [ 25 | "SwiftFormats" 26 | ] 27 | ), 28 | .testTarget( 29 | name: "SwiftFieldsTests", 30 | dependencies: [ 31 | "SwiftFields" 32 | ] 33 | ), 34 | ] 35 | ) 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SwiftFields 2 | 3 | SwiftFields is a collection of SwiftUI widgets for editing data. It was originally created with the aim of providing widgets for editing values in a 3D editor such as angles, vectors, matrixes, quaternions, etc. 4 | 5 | This library is in early development. 6 | 7 | Current widgets: 8 | 9 | - ``AngleEditor``: A widget for editing angles. ![Screenshot of AngleEditor](Documentation/AngleEditorDemo.png) 10 | - ``ClosedRangedSlider``: A slider for editing a ClosedRange of Doubles. ![Screenshot of ClosedRangeSlider](Documentation/ClosedRangeSliderDemo.png) 11 | - ``PathSlider``: Like SwiftUI's `Slider` but you can slide the thumb along any arbitrary path. ![Screenshot of PathSlider](Documentation/PathSliderDemo.png) 12 | - ``YASlider``: Like SwiftUI's `Slider` but with more functionality including vertical orientation, ~~custom thumb, and custom track~~. (Built on top of ``PathSlider``) ![Screenshot of YASlider](Documentation/YASliderDemo.png) 13 | 14 | ## TODO 15 | 16 | - [X]: Use vertical slider for AngleEditor 17 | - [ ]: Finish angle editor range limits 18 | - [X]: Accessibility for AngleEditor 19 | - [ ]: Highlight thumb for PathSlider 20 | - [ ]: Dark mode support for PathSlider 21 | - [ ]: Get inner shadow/glow working for PathSlider 22 | - [ ]: Accessibility for ClosedRangeSlider 23 | - [ ]: Make YASlider (PathSlider?) support labels. 24 | - [ ]: Angle editor in dark mode 25 | - [ ]: iOS pass on all widgets 26 | -------------------------------------------------------------------------------- /Sources/SwiftFields/AngleEditor.swift: -------------------------------------------------------------------------------- 1 | #if !os(tvOS) 2 | import SwiftFormats 3 | import SwiftUI 4 | 5 | // https://mastodon.social/@ikenndac/110316785167632103 6 | 7 | public struct AngleEditor: View { 8 | fileprivate struct Geometry { 9 | var canvasDiameter: CGFloat 10 | var borderWidth: CGFloat 11 | var edgeWidth: CGFloat 12 | } 13 | 14 | @Binding 15 | private var angle: Angle 16 | 17 | @Environment(\.controlSize) 18 | private var controlSize 19 | 20 | private let limit: ClosedRange 21 | 22 | public init(angle: Binding, limit: ClosedRange = .degrees(0) ... .degrees(360)) { 23 | self._angle = angle 24 | self.limit = limit 25 | } 26 | 27 | public var body: some View { 28 | let geometry = Geometry(controlSize: controlSize) 29 | let shadowRadius = 1.0 30 | let color = Color.red 31 | 32 | return VStack { 33 | TextField("Angle", value: $angle, format: .angle) 34 | HStack { 35 | Canvas { context, size in 36 | context.drawLayer { context in 37 | let center = CGPoint(x: size.width * 0.5, y: size.height * 0.5) 38 | let radius = min(size.width, size.height) / 2 - geometry.borderWidth - shadowRadius 39 | 40 | let startLimitAngle = Angle(degrees: limit.lowerBound.degrees - 180) 41 | let endLimitAngle = Angle(degrees: limit.upperBound.degrees - 180) 42 | 43 | let limitArc = Path.arc(center: center, radius: radius, startAngle: startLimitAngle, endAngle: endLimitAngle, clockwise: false, closed: true) 44 | context.fill(limitArc, with: .color(.black.opacity(0.1))) 45 | //context.fill(limitArc, with: .color(color)) 46 | 47 | let startAngle = Angle(degrees: 0 - angle.degrees / 2 - 90) 48 | let endAngle = Angle(degrees: 0 + angle.degrees / 2 - 90) 49 | let angleArc = Path.arc(center: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: false, closed: true) 50 | context.fill(angleArc, with: .color(color.opacity(0.5))) 51 | if angle.degrees != 360 { 52 | let arcEdges = Path { path in 53 | path.move(to: center) 54 | path.addLine(to: center + CGPoint(x: radius, y: 0).rotated(by: startAngle)) 55 | if angle.degrees != 0 { 56 | path.move(to: center) 57 | path.addLine(to: center + CGPoint(x: radius, y: 0).rotated(by: endAngle)) 58 | } 59 | } 60 | context.stroke(arcEdges, with: .color(color), style: .init(lineWidth: geometry.edgeWidth, dash: [geometry.edgeWidth * 2, geometry.edgeWidth * 2], dashPhase: geometry.edgeWidth * 2)) 61 | } 62 | context.stroke(limitArc, with: .color(.white), style: .init(lineWidth: geometry.borderWidth)) 63 | } 64 | context.addFilter(.shadow(radius: shadowRadius)) 65 | } 66 | .frame(width: geometry.canvasDiameter) 67 | .aspectRatio(1.0, contentMode: .fit) 68 | YASlider(value: $angle.degrees, in: limit.lowerBound.degrees ... limit.upperBound.degrees, axis: .vertical) 69 | } 70 | .frame(height: geometry.canvasDiameter) 71 | } 72 | .shadow(radius: shadowRadius) 73 | .accessibilityRepresentation { 74 | Slider(value: $angle.degrees, in: 0 ... 360) 75 | } 76 | } 77 | } 78 | 79 | // MARK: - 80 | 81 | extension AngleEditor.Geometry { 82 | init(controlSize: ControlSize) { 83 | switch controlSize { 84 | case .mini: 85 | canvasDiameter = 32 86 | borderWidth = 1 87 | edgeWidth = 1 88 | case .small: 89 | canvasDiameter = 40 90 | borderWidth = 2 91 | edgeWidth = 2 92 | case .regular: 93 | canvasDiameter = 64 94 | borderWidth = 2 95 | edgeWidth = 2 96 | case .large: 97 | canvasDiameter = 80 98 | borderWidth = 4 99 | edgeWidth = 3 100 | case .extraLarge: 101 | canvasDiameter = 80 102 | borderWidth = 4 103 | edgeWidth = 3 104 | @unknown default: 105 | canvasDiameter = 64 106 | borderWidth = 4 107 | edgeWidth = 2 108 | } 109 | } 110 | } 111 | 112 | struct AngleEditorPreview: PreviewProvider { 113 | static var previews: some View { 114 | let angle = Binding.constant(Angle(degrees: 160)) 115 | let limit: ClosedRange = .degrees(0) ... .degrees(180) 116 | 117 | HStack { 118 | AngleEditor(angle: angle, limit: limit) 119 | .controlSize(.mini) 120 | .border(.black.opacity(0.25)) 121 | AngleEditor(angle: angle, limit: limit) 122 | .controlSize(.small) 123 | .border(.black.opacity(0.25)) 124 | AngleEditor(angle: angle, limit: limit) 125 | .controlSize(.regular) 126 | .border(.black.opacity(0.25)) 127 | AngleEditor(angle: angle, limit: limit) 128 | .controlSize(.large) 129 | .border(.black.opacity(0.25)) 130 | } 131 | } 132 | } 133 | #endif 134 | -------------------------------------------------------------------------------- /Sources/SwiftFields/ClosedRangeSlider.swift: -------------------------------------------------------------------------------- 1 | #if !os(tvOS) 2 | import SwiftUI 3 | 4 | public struct ClosedRangeSlider: View { 5 | @Binding 6 | private var value: ClosedRange 7 | 8 | private let lowerLimit: ClosedRange // TODO: not used yet 9 | private let upperLimit: ClosedRange // TODO: not used yet 10 | 11 | public init(value: Binding>, lowerLimit: ClosedRange = 0 ... 1, upperLimit: ClosedRange = 0 ... 1) { 12 | self._value = value 13 | self.lowerLimit = lowerLimit 14 | self.upperLimit = upperLimit 15 | } 16 | 17 | public var body: some View { 18 | let lowerBound = Binding { 19 | return value.lowerBound 20 | } set: { newValue in 21 | value = min(newValue, value.upperBound) ... value.upperBound 22 | } 23 | let upperBound = Binding { 24 | return value.upperBound 25 | } set: { newValue in 26 | value = value.lowerBound ... max(newValue, value.lowerBound) 27 | } 28 | GeometryReader { proxy in 29 | let linePath = Path.line(from: CGPoint(x: 5, y: 10), to: CGPoint(x: proxy.size.width - 5, y: 10)) 30 | let path = Path.line(from: CGPoint(x: 10, y: 10), to: CGPoint(x: proxy.size.width - 10, y: 10)) 31 | ZStack { 32 | linePath.trimmedPath(from: 0, to: 1).stroke(Color(white: 0.87), style: .init(lineWidth: 4, lineCap: .round)) 33 | linePath.trimmedPath(from: value.lowerBound, to: value.upperBound).stroke(Color.accentColor, style: .init(lineWidth: 4, lineCap: .round)) 34 | 35 | PathSliderHelper(value: lowerBound, path: path) { 36 | Thumb { 37 | ArcShape(angle: .degrees(180), width: .degrees(180)) 38 | } 39 | .frame(width: 20, height: 20) 40 | } 41 | PathSliderHelper(value: upperBound, path: path) { 42 | Thumb { 43 | ArcShape(angle: .degrees(0), width: .degrees(180)) 44 | } 45 | .frame(width: 20, height: 20) 46 | } 47 | } 48 | } 49 | .frame(height: 20) 50 | } 51 | } 52 | #endif 53 | -------------------------------------------------------------------------------- /Sources/SwiftFields/PathSlider.swift: -------------------------------------------------------------------------------- 1 | #if !os(tvOS) 2 | import SwiftUI 3 | 4 | public struct PathSlider: View { 5 | @Binding 6 | private var value: Double 7 | 8 | @Environment(\.controlSize) 9 | var controlSize 10 | 11 | #if os(macOS) 12 | @Environment(\.controlActiveState) 13 | var controlActiveState 14 | #endif 15 | 16 | @Environment(\.scenePhase) 17 | var scenePhase 18 | 19 | private let range: ClosedRange 20 | private let trackPath: Path 21 | private let thumbPath: Path 22 | 23 | public init(value: Binding, in range: ClosedRange = 0 ... 1, trackPath: Path, thumbPath: Path) { 24 | self._value = value 25 | self.range = range 26 | self.trackPath = trackPath 27 | self.thumbPath = thumbPath 28 | } 29 | 30 | private var geometry: PathSliderGeometry { 31 | return controlSize.pathSliderGeometry 32 | } 33 | 34 | private var binding: Binding { 35 | Binding { 36 | return (value - range.lowerBound) / (range.upperBound - range.lowerBound) 37 | } set: { newValue in 38 | value = newValue * (range.upperBound - range.lowerBound) + range.lowerBound 39 | } 40 | } 41 | 42 | public var body: some View { 43 | ZStack { 44 | trackPath.stroke(Color.sliderBackground, style: .init(lineWidth: geometry.trackWidth, lineCap: .round)) 45 | //trackPath.stroke(.shadow(.inner(color: .pink.opacity(0.0), radius: 1)), style: .init(lineWidth: geometry.trackWidth, lineCap: .round)) 46 | trackPath.trimmedPath(from: 0, to: binding.wrappedValue).stroke(activeTrackColor, style: .init(lineWidth: geometry.trackWidth, lineCap: .round)) 47 | PathSliderHelper(value: binding, path: thumbPath) { 48 | Thumb { 49 | Circle() 50 | } 51 | .frame(width: geometry.thumbSize.width, height: geometry.thumbSize.height) 52 | #if os(macOS) 53 | .accessibilityElement() 54 | .accessibilityValue("\(value, format: .number)") 55 | #endif 56 | } 57 | } 58 | #if os(iOS) 59 | .accessibilityRepresentation(representation: { 60 | Slider(value: $value, in: range) 61 | }) 62 | #elseif os(macOS) 63 | .accessibilityElement(children: .contain) 64 | .accessibilityAdjustableAction { direction in 65 | switch direction { 66 | case .increment: 67 | value += (range.upperBound / range.lowerBound) / 10 68 | case .decrement: 69 | value -= (range.upperBound / range.lowerBound) / 10 70 | @unknown default: 71 | break 72 | } 73 | } 74 | .accessibilityCustomContent(AccessibilityCustomContentKey(Text("FOO"), id: "FOO"), Text("FOO")) 75 | #endif 76 | } 77 | 78 | var activeTrackColor: Color { 79 | #if os(macOS) 80 | switch controlActiveState { 81 | case .active, .key: 82 | return .accentColor 83 | case .inactive: 84 | return .sliderBackground 85 | @unknown default: 86 | return .accentColor 87 | } 88 | #else 89 | return .accentColor 90 | #endif 91 | } 92 | } 93 | 94 | public extension PathSlider { 95 | init(value: Binding, in range: ClosedRange = 0 ... 1, path: Path) { 96 | self.init(value: value, in: range, trackPath: path, thumbPath: path) 97 | } 98 | } 99 | 100 | // MARK: - 101 | 102 | internal struct PathSliderHelper : View where Thumb: View { 103 | @Binding 104 | private var value: Double 105 | 106 | private let path: Path 107 | private let segments: PathSegments 108 | private let thumb: Thumb 109 | 110 | init(value: Binding, path: Path, segments: Int = 100, thumb: () -> Thumb) { 111 | self._value = value 112 | self.path = path 113 | self.thumb = thumb() 114 | self.segments = PathSegments(path: path, segments: segments) 115 | } 116 | 117 | var body: some View { 118 | thumb.position(segments.segment(for: value)).gesture(thumbDragGesture) 119 | } 120 | 121 | private var thumbDragGesture: some Gesture { 122 | DragGesture().onChanged { value in 123 | self.value = segments.value(for: value.location) 124 | } 125 | } 126 | } 127 | 128 | struct PathSlider_Preview: PreviewProvider { 129 | static var previews: some View { 130 | PathSlider(value: .constant(0), trackPath: Path.horizontalLine(from: 0, to: 100).offsetBy(dx: 0, dy: 10), thumbPath: Path.horizontalLine(from: 10, to: 90).offsetBy(dx: 0, dy: 10)) 131 | .frame(width: 100, height: 20) 132 | } 133 | } 134 | #endif 135 | -------------------------------------------------------------------------------- /Sources/SwiftFields/Support.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | internal struct ArcShape: SwiftUI.Shape { 4 | let angle: SwiftUI.Angle 5 | let width: SwiftUI.Angle 6 | 7 | func path(in rect: CGRect) -> Path { 8 | Path { path in 9 | let center = CGPoint(x: rect.midX, y: rect.midY) 10 | let radius = min(rect.width, rect.height) / 2 11 | let startAngle = Angle.radians(angle.radians - width.radians / 2) 12 | let endAngle = Angle.radians(angle.radians + width.radians / 2) 13 | path.move(to: center) 14 | path.addArc(center: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: false) 15 | path.closeSubpath() 16 | } 17 | } 18 | } 19 | 20 | internal extension CGPoint { 21 | func rotated(by angle: SwiftUI.Angle) -> CGPoint { 22 | applying(CGAffineTransform(rotationAngle: angle.radians)) 23 | } 24 | 25 | static func + (lhs: CGPoint, rhs: CGPoint) -> CGPoint { 26 | return CGPoint(x: lhs.x + rhs.x, y: lhs.y + rhs.y) 27 | } 28 | } 29 | 30 | internal extension Path { 31 | static func circle(center: CGPoint, radius: CGFloat) -> Path { 32 | return Path(ellipseIn: CGRect(x: center.x - radius, y: center.y - radius, width: radius * 2, height: radius * 2)) 33 | } 34 | 35 | // swiftlint:disable:next function_parameter_count 36 | static func arc(center: CGPoint, radius: CGFloat, startAngle: Angle, endAngle: Angle, clockwise: Bool, closed: Bool) -> Path { 37 | if endAngle.degrees - startAngle.degrees >= 360 { 38 | return .circle(center: center, radius: radius) 39 | } 40 | 41 | return Path { path in 42 | if closed { 43 | path.move(to: center) 44 | } 45 | path.addArc(center: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: clockwise) 46 | if closed { 47 | path.closeSubpath() 48 | } 49 | } 50 | } 51 | 52 | static func line(from: CGPoint, to: CGPoint) -> Path { 53 | return Path { path in 54 | path.addLines([from, to]) 55 | } 56 | } 57 | 58 | static func horizontalLine(from: CGFloat, to: CGFloat) -> Path { 59 | return Path { path in 60 | path.addLines([CGPoint(x: from, y: 0), CGPoint(x: to, y: 0)]) 61 | } 62 | } 63 | } 64 | 65 | internal extension CGPoint { 66 | func distanceSquared(to other: CGPoint) -> CGFloat { 67 | (x - other.x) * (x - other.x) + (y - other.y) * (y - other.y) 68 | } 69 | } 70 | 71 | internal extension Path { 72 | var startPoint: CGPoint? { 73 | let r = trimmedPath(from: 0, to: 0.00001).boundingRect 74 | return CGPoint(x: r.midX, y: r.midY) 75 | } 76 | } 77 | 78 | internal struct PathSegments { 79 | let segments: [CGPoint] 80 | 81 | init(path: Path, segments: Int) { 82 | assert(segments > 0) 83 | self.segments = 84 | [path.startPoint!] 85 | + (0 ..< segments).reduce(into: []) { partialResult, segment in 86 | let from = Double(segment) / Double(segments) 87 | let to = Double(segment + 1) / Double(segments) 88 | 89 | let rect = path.trimmedPath(from: from, to: to).boundingRect 90 | partialResult.append(CGPoint(x: rect.midX, y: rect.midY)) 91 | } 92 | + [ path.currentPoint! ] 93 | } 94 | 95 | func value(for point: CGPoint) -> Double { 96 | guard let firstSegment = segments.first else { 97 | fatalError("No segments.") 98 | } 99 | var lowestDistance = firstSegment.distanceSquared(to: point) 100 | var closestSegmentIndex = 0 101 | segments.enumerated().dropFirst().forEach { index, segment in 102 | let distance = segment.distanceSquared(to: point) 103 | if distance < lowestDistance { 104 | lowestDistance = distance 105 | closestSegmentIndex = index 106 | } 107 | } 108 | return Double(closestSegmentIndex) / Double(segments.count - 1) 109 | } 110 | 111 | func segment(for value: Double) -> CGPoint { 112 | return segments[min(Int(value * Double(segments.count)), segments.count - 1)] 113 | } 114 | } 115 | 116 | internal extension Color { 117 | static let sliderBackground = Color(white: 0.875) 118 | } 119 | 120 | internal struct Thumb : View where S: Shape { 121 | let shape: S 122 | 123 | init(_ shape: () -> S) { 124 | self.shape = shape() 125 | } 126 | 127 | var body: some View { 128 | ZStack { 129 | shape.fill(Color.white).shadow(color: Color(.sRGBLinear, white: 0, opacity: 0.05), radius: 0.5, y: 2) 130 | shape.stroke(Color.sliderBackground) 131 | } 132 | } 133 | } 134 | 135 | public struct PathSliderGeometry { 136 | public var thumbSize: CGSize 137 | public var trackWidth: CGFloat 138 | 139 | #if os(tvOS) 140 | static let tvOS = PathSliderGeometry(thumbSize: CGSize(width: 20, height: 20), trackWidth: 4) 141 | #else 142 | public init(_ controlSize: ControlSize) { 143 | switch controlSize { 144 | case .mini: 145 | thumbSize = CGSize(width: 14, height: 14) 146 | trackWidth = 3 147 | case .small: 148 | thumbSize = CGSize(width: 16, height: 16) 149 | trackWidth = 3 150 | case .regular: 151 | thumbSize = CGSize(width: 20, height: 20) 152 | trackWidth = 4 153 | case .large: 154 | thumbSize = CGSize(width: 20, height: 20) 155 | trackWidth = 4 156 | case .extraLarge: 157 | thumbSize = CGSize(width: 20, height: 20) 158 | trackWidth = 4 159 | @unknown default: 160 | thumbSize = CGSize(width: 20, height: 20) 161 | trackWidth = 4 162 | } 163 | } 164 | #endif 165 | } 166 | 167 | #if !os(tvOS) 168 | internal extension ControlSize { 169 | var pathSliderGeometry: PathSliderGeometry { 170 | return PathSliderGeometry(self) 171 | } 172 | } 173 | #endif 174 | 175 | internal struct LineSegment: Equatable { 176 | var from: CGPoint 177 | var to: CGPoint 178 | 179 | init(from: CGPoint, to: CGPoint) { 180 | self.from = from 181 | self.to = to 182 | } 183 | } 184 | 185 | internal extension LineSegment { 186 | 187 | init(x1: CGFloat, y1: CGFloat, x2: CGFloat, y2: CGFloat) { 188 | self.init(from: CGPoint(x: x1, y: y1), to: CGPoint(x: x2, y: y2)) 189 | } 190 | 191 | init(x: CGFloat, from y1: CGFloat, to y2: CGFloat) { 192 | self.init(from: CGPoint(x: x, y: y1), to: CGPoint(x: x, y: y2)) 193 | } 194 | 195 | init(y: CGFloat, from x1: CGFloat, to x2: CGFloat) { 196 | self.init(from: CGPoint(x: x1, y: y), to: CGPoint(x: x2, y: y)) 197 | } 198 | 199 | init(axis: Axis, from: CGFloat, to: CGFloat) { 200 | switch axis { 201 | case .horizontal: 202 | self.init(from: CGPoint(x: from, y: 0), to: CGPoint(x: to, y: 0)) 203 | case .vertical: 204 | self.init(from: CGPoint(x: 0, y: from), to: CGPoint(x: 0, y: to)) 205 | } 206 | } 207 | 208 | var boundingRect: CGRect { 209 | return CGRect(x: min(from.x, to.x), y: min(from.y, to.y), width: abs(from.x - to.x), height: abs(from.y - to.y)) 210 | } 211 | 212 | func insetBy(dx: CGFloat = 0, dy: CGFloat = 0) -> LineSegment { 213 | var copy = self 214 | if from.x <= to.x { 215 | copy.from.x += dx 216 | copy.to.x -= dx 217 | } 218 | else { 219 | copy.from.x -= dx 220 | copy.to.x += dx 221 | } 222 | if from.y <= to.y { 223 | copy.from.y += dy 224 | copy.to.y -= dy 225 | } 226 | else { 227 | copy.from.y -= dy 228 | copy.to.y += dy 229 | } 230 | return copy 231 | } 232 | 233 | func insetBy(_ point: CGPoint) -> LineSegment { 234 | insetBy(dx: point.x, dy: point.y) 235 | } 236 | 237 | func offsetBy(dx: CGFloat = 0, dy: CGFloat = 0) -> LineSegment { 238 | var copy = self 239 | copy.from.x += dx 240 | copy.from.y += dy 241 | copy.to.x += dx 242 | copy.to.y += dy 243 | return copy 244 | } 245 | 246 | func offsetBy(_ point: CGPoint) -> LineSegment { 247 | offsetBy(dx: point.x, dy: point.y) 248 | } 249 | } 250 | 251 | internal extension Path { 252 | init(_ lineSegment: LineSegment) { 253 | self = Path { path in 254 | path.addLines([lineSegment.from, lineSegment.to]) 255 | } 256 | } 257 | } 258 | 259 | internal extension CGPoint { 260 | init(axis: Axis, length: CGFloat) { 261 | switch axis { 262 | case .horizontal: 263 | self.init(x: length, y: 0) 264 | case .vertical: 265 | self.init(x: 0, y: length) 266 | } 267 | } 268 | } 269 | 270 | internal extension LineSegment { 271 | func flipped() -> LineSegment { 272 | return LineSegment(from: to, to: from) 273 | } 274 | } 275 | 276 | internal extension Axis { 277 | static prefix func !(value: Self) -> Axis { 278 | switch value { 279 | case .horizontal: 280 | return .vertical 281 | case .vertical: 282 | return .horizontal 283 | } 284 | } 285 | } 286 | -------------------------------------------------------------------------------- /Sources/SwiftFields/YASlider.swift: -------------------------------------------------------------------------------- 1 | #if !os(tvOS) 2 | import SwiftUI 3 | 4 | public struct YASlider: View { 5 | @Binding 6 | private var value: Double 7 | 8 | @Environment(\.controlSize) 9 | private var controlSize 10 | 11 | private let limit: ClosedRange 12 | private let axis: Axis 13 | 14 | public init(value: Binding, in limit: ClosedRange = 0 ... 1, axis: Axis) { 15 | self._value = value 16 | self.limit = limit 17 | self.axis = axis 18 | } 19 | 20 | public var body: some View { 21 | let geometry = controlSize.pathSliderGeometry 22 | GeometryReader { proxy in 23 | let (trackPath, thumbPath) = paths(for: proxy.size) 24 | PathSlider(value: _value, in: limit, trackPath: trackPath, thumbPath: thumbPath) 25 | } 26 | .frame(width: axis == .vertical ? geometry.thumbSize.height : nil, height: axis == .horizontal ? geometry.thumbSize.height : nil) 27 | .frame(minWidth: axis == .horizontal ? geometry.thumbSize.width : nil, minHeight: axis == .vertical ? geometry.thumbSize.height : nil) 28 | } 29 | 30 | private func paths(for size: CGSize) -> (trackPath: Path, thumbPath: Path) { 31 | let geometry = controlSize.pathSliderGeometry 32 | let halfThumbSize = CGSize(width: geometry.thumbSize.width * 0.5, height: geometry.thumbSize.height * 0.5) 33 | let length = axis == .horizontal ? size.width : size.height 34 | var line = LineSegment(axis: axis, from: 0, to: length) 35 | if axis == .vertical { 36 | line = line.flipped() 37 | } 38 | let trackPath = line 39 | .insetBy(CGPoint(axis: axis, length: geometry.trackWidth / 2)) 40 | .offsetBy(CGPoint(axis: !axis, length: halfThumbSize.height)) 41 | let thumbPath = line 42 | .insetBy(CGPoint(axis: axis, length: halfThumbSize.width)) 43 | .offsetBy(CGPoint(axis: !axis, length: halfThumbSize.height)) 44 | return (trackPath: Path(trackPath), thumbPath: Path(thumbPath)) 45 | } 46 | } 47 | 48 | // MARK: - 49 | 50 | struct YASlider_Preview: PreviewProvider { 51 | static var previews: some View { 52 | VStack { 53 | YASlider(value: .constant(0.5), axis: .horizontal) 54 | YASlider(value: .constant(0.5), axis: .vertical).frame(height: 50) 55 | } 56 | } 57 | } 58 | #endif 59 | -------------------------------------------------------------------------------- /Tests/SwiftFieldsTests/SwiftFieldsTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import SwiftFields 3 | 4 | final class LineSegmentTest: XCTestCase { 5 | func test1() throws { 6 | let l1 = LineSegment(x1: 0, y1: 0, x2: 10, y2: 0).insetBy(dx: 2.5, dy: 0) 7 | XCTAssertEqual(l1, LineSegment(x1: 2.5, y1: 0, x2: 7.5, y2: 0)) 8 | let l2 = LineSegment(x1: 0, y1: 0, x2: 10, y2: 0).insetBy(dx: -1, dy: 0) 9 | XCTAssertEqual(l2, LineSegment(x1: -1, y1: 0, x2: 11, y2: 0)) 10 | 11 | let r1 = LineSegment(x1: 0, y1: 0, x2: 10, y2: 10).boundingRect 12 | XCTAssertEqual(r1, CGRect(x: 0, y: 0, width: 10, height: 10)) 13 | let r2 = LineSegment(x1: 5, y1: -5, x2: 10, y2: 10).boundingRect 14 | XCTAssertEqual(r2, CGRect(x: 5, y: -5, width: 5, height: 15)) 15 | 16 | } 17 | } 18 | --------------------------------------------------------------------------------