├── .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. 
10 | - ``ClosedRangedSlider``: A slider for editing a ClosedRange of Doubles. 
11 | - ``PathSlider``: Like SwiftUI's `Slider` but you can slide the thumb along any arbitrary path. 
12 | - ``YASlider``: Like SwiftUI's `Slider` but with more functionality including vertical orientation, ~~custom thumb, and custom track~~. (Built on top of ``PathSlider``) 
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 |
--------------------------------------------------------------------------------