diff --git a/CHANGELOG.md b/CHANGELOG.md index f280ba8..2c616b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # CHANGELOG +## [0.1.1] - 2024-03-17 + +### Changed +- Updated changelog + +## [0.1.0] - 2024-03-17 + +### Added +- Added new configurations for direction-specific animations +- Refined animation transitions when changing directions for a more fluid user experience +- SliderDelegate now supports Swift Concurrency for asynchronous event handling + +### Changed +- Rendering of changes is now synchronized with the GPU using CADisplayLink for smoother visual updates. +- Removed support for Interface Builder to streamline codebase and improve programmability. + ## [0.0.9] - 2024-03-14 ### Fixed diff --git a/README.md b/README.md index cad6fa1..1f4b104 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,14 @@ - Swift 5+ ## Preview - +
+ Preview + + + + + +
## Installation @@ -29,7 +36,7 @@ Once you have your Swift package set up, adding Slider as a dependency is as eas ```swift dependencies: [ - .package(url: "https://github.com/Ramiz69/Slider.git", .upToNextMajor(from: "0.0.9")) + .package(url: "https://github.com/Ramiz69/Slider.git", .upToNextMajor(from: "0.1.0")) ] ``` diff --git a/RKSlider.podspec b/RKSlider.podspec index 2ec9076..6d854db 100644 --- a/RKSlider.podspec +++ b/RKSlider.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |spec| spec.name = 'RKSlider' - spec.version = '0.0.9' + spec.version = '0.1.1' spec.summary = 'A CocoaPods library written in Swift' spec.description = <<-DESC diff --git a/RKSlider/0.1.0/RKSlider.podspec b/RKSlider/0.1.0/RKSlider.podspec new file mode 100644 index 0000000..4048b4b --- /dev/null +++ b/RKSlider/0.1.0/RKSlider.podspec @@ -0,0 +1,26 @@ +Pod::Spec.new do |spec| + + spec.name = 'RKSlider' + spec.version = '0.1.0' + spec.summary = 'A CocoaPods library written in Swift' + + spec.description = <<-DESC +This CocoaPods library helps you create application with the best slider. + DESC + + spec.homepage = 'https://github.com/Ramiz69/Slider' + + spec.license = 'MIT' + + spec.author = { 'Ramiz Kichibekov' => 'ramiz161@icloud.com' } + spec.social_media_url = 'https://t.me/Ramiz69' + + spec.ios.deployment_target = "14.0" + + spec.source = { :git => 'https://github.com/Ramiz69/Slider.git', :tag => spec.version } + + spec.swift_version = ['5.0', '5.9'] + + spec.source_files = 'Sources/*.swift', 'Sources/**/*.swift' + +end diff --git a/Screenshots/leftToRightCustom.png b/Screenshots/leftToRightCustom.png new file mode 100644 index 0000000..4b22a6b Binary files /dev/null and b/Screenshots/leftToRightCustom.png differ diff --git a/Screenshots/leftToRightDefault.png b/Screenshots/leftToRightDefault.png new file mode 100644 index 0000000..3cc0406 Binary files /dev/null and b/Screenshots/leftToRightDefault.png differ diff --git a/Screenshots/preference.png b/Screenshots/preference.png new file mode 100644 index 0000000..51a58ab Binary files /dev/null and b/Screenshots/preference.png differ diff --git a/Screenshots/rightToLeftDefault.png b/Screenshots/rightToLeftDefault.png new file mode 100644 index 0000000..602d2a4 Binary files /dev/null and b/Screenshots/rightToLeftDefault.png differ diff --git a/Slider.xcodeproj/project.pbxproj b/Slider.xcodeproj/project.pbxproj index 618f7b0..6567d95 100644 --- a/Slider.xcodeproj/project.pbxproj +++ b/Slider.xcodeproj/project.pbxproj @@ -16,14 +16,21 @@ 4B2EB6F123C1E5AA00FB91AD /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 4B2EB6EF23C1E5AA00FB91AD /* LaunchScreen.storyboard */; }; 4B889B692433375D002FCE02 /* CodeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B889B682433375D002FCE02 /* CodeViewController.swift */; }; 4B889B7624333E7C002FCE02 /* CodeViewController+SliderDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B889B7524333E7C002FCE02 /* CodeViewController+SliderDelegate.swift */; }; + 4B8DAE962BA6925A005CFA82 /* Animation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B8DAE952BA6925A005CFA82 /* Animation.swift */; }; + 4B8DAE982BA6956B005CFA82 /* TrackConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B8DAE972BA6956B005CFA82 /* TrackConfiguration.swift */; }; + 4B8DAE9A2BA696A8005CFA82 /* ThumbConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B8DAE992BA696A8005CFA82 /* ThumbConfiguration.swift */; }; + 4B8DAE9C2BA69A69005CFA82 /* RangeEndpointsConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B8DAE9B2BA69A69005CFA82 /* RangeEndpointsConfiguration.swift */; }; + 4B8DAE9F2BA6A056005CFA82 /* PreferenceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B8DAE9E2BA6A056005CFA82 /* PreferenceManager.swift */; }; + 4B8DAEA12BA6A5E9005CFA82 /* CodeViewController+UIColorPickerViewControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B8DAEA02BA6A5E9005CFA82 /* CodeViewController+UIColorPickerViewControllerDelegate.swift */; }; + 4B8DAEA42BA6AE54005CFA82 /* ThumbLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B8DAEA22BA6ACFF005CFA82 /* ThumbLayer.swift */; }; + 4B8DAEA62BA6B445005CFA82 /* TextLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B8DAEA52BA6B445005CFA82 /* TextLayer.swift */; }; 4BF628A12BA2B38D0093637E /* HapticConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BF628922BA2B38D0093637E /* HapticConfiguration.swift */; }; - 4BF628A22BA2B38D0093637E /* DirectionEnum.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BF628942BA2B38D0093637E /* DirectionEnum.swift */; }; + 4BF628A22BA2B38D0093637E /* Direction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BF628942BA2B38D0093637E /* Direction.swift */; }; 4BF628A32BA2B38D0093637E /* Slider+UITouch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BF628962BA2B38D0093637E /* Slider+UITouch.swift */; }; 4BF628A42BA2B38D0093637E /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BF628972BA2B38D0093637E /* String.swift */; }; - 4BF628A52BA2B38D0093637E /* Protocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BF628992BA2B38D0093637E /* Protocols.swift */; }; + 4BF628A52BA2B38D0093637E /* SliderDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BF628992BA2B38D0093637E /* SliderDelegate.swift */; }; 4BF628A72BA2B38D0093637E /* Slider.h in Headers */ = {isa = PBXBuildFile; fileRef = 4BF6289C2BA2B38D0093637E /* Slider.h */; }; 4BF628A82BA2B38D0093637E /* Slider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BF6289D2BA2B38D0093637E /* Slider.swift */; }; - 4BF628A92BA2B38D0093637E /* SliderTextLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BF6289E2BA2B38D0093637E /* SliderTextLayer.swift */; }; 4BF628AA2BA2B38D0093637E /* SliderTrackLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BF6289F2BA2B38D0093637E /* SliderTrackLayer.swift */; }; /* End PBXBuildFile section */ @@ -62,15 +69,22 @@ 4B2EB6F223C1E5AA00FB91AD /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 4B889B682433375D002FCE02 /* CodeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodeViewController.swift; sourceTree = ""; }; 4B889B7524333E7C002FCE02 /* CodeViewController+SliderDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CodeViewController+SliderDelegate.swift"; sourceTree = ""; }; + 4B8DAE952BA6925A005CFA82 /* Animation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Animation.swift; sourceTree = ""; }; + 4B8DAE972BA6956B005CFA82 /* TrackConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackConfiguration.swift; sourceTree = ""; }; + 4B8DAE992BA696A8005CFA82 /* ThumbConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThumbConfiguration.swift; sourceTree = ""; }; + 4B8DAE9B2BA69A69005CFA82 /* RangeEndpointsConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RangeEndpointsConfiguration.swift; sourceTree = ""; }; + 4B8DAE9E2BA6A056005CFA82 /* PreferenceManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferenceManager.swift; sourceTree = ""; }; + 4B8DAEA02BA6A5E9005CFA82 /* CodeViewController+UIColorPickerViewControllerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CodeViewController+UIColorPickerViewControllerDelegate.swift"; sourceTree = ""; }; + 4B8DAEA22BA6ACFF005CFA82 /* ThumbLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThumbLayer.swift; sourceTree = ""; }; + 4B8DAEA52BA6B445005CFA82 /* TextLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextLayer.swift; sourceTree = ""; }; 4BF628922BA2B38D0093637E /* HapticConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HapticConfiguration.swift; sourceTree = ""; }; - 4BF628942BA2B38D0093637E /* DirectionEnum.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DirectionEnum.swift; sourceTree = ""; }; + 4BF628942BA2B38D0093637E /* Direction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Direction.swift; sourceTree = ""; }; 4BF628962BA2B38D0093637E /* Slider+UITouch.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Slider+UITouch.swift"; sourceTree = ""; }; 4BF628972BA2B38D0093637E /* String.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = ""; }; - 4BF628992BA2B38D0093637E /* Protocols.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Protocols.swift; sourceTree = ""; }; + 4BF628992BA2B38D0093637E /* SliderDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SliderDelegate.swift; sourceTree = ""; }; 4BF6289B2BA2B38D0093637E /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 4BF6289C2BA2B38D0093637E /* Slider.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Slider.h; sourceTree = ""; }; 4BF6289D2BA2B38D0093637E /* Slider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Slider.swift; sourceTree = ""; }; - 4BF6289E2BA2B38D0093637E /* SliderTextLayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SliderTextLayer.swift; sourceTree = ""; }; 4BF6289F2BA2B38D0093637E /* SliderTrackLayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SliderTrackLayer.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -115,6 +129,7 @@ 4B2EB6E323C1E5AA00FB91AD /* Slider_Example */ = { isa = PBXGroup; children = ( + 4B8DAE9D2BA6A04B005CFA82 /* Manager */, 4B889B6A24333B9F002FCE02 /* Application */, 4B889B6B24333BAB002FCE02 /* Flows */, 4B889B6F24333BC8002FCE02 /* Supporting Files */, @@ -186,14 +201,26 @@ isa = PBXGroup; children = ( 4B889B7524333E7C002FCE02 /* CodeViewController+SliderDelegate.swift */, + 4B8DAEA02BA6A5E9005CFA82 /* CodeViewController+UIColorPickerViewControllerDelegate.swift */, ); path = Extensions; sourceTree = ""; }; + 4B8DAE9D2BA6A04B005CFA82 /* Manager */ = { + isa = PBXGroup; + children = ( + 4B8DAE9E2BA6A056005CFA82 /* PreferenceManager.swift */, + ); + path = Manager; + sourceTree = ""; + }; 4BF628932BA2B38D0093637E /* Configurations */ = { isa = PBXGroup; children = ( 4BF628922BA2B38D0093637E /* HapticConfiguration.swift */, + 4B8DAE972BA6956B005CFA82 /* TrackConfiguration.swift */, + 4B8DAE992BA696A8005CFA82 /* ThumbConfiguration.swift */, + 4B8DAE9B2BA69A69005CFA82 /* RangeEndpointsConfiguration.swift */, ); path = Configurations; sourceTree = ""; @@ -201,7 +228,8 @@ 4BF628952BA2B38D0093637E /* Enums */ = { isa = PBXGroup; children = ( - 4BF628942BA2B38D0093637E /* DirectionEnum.swift */, + 4BF628942BA2B38D0093637E /* Direction.swift */, + 4B8DAE952BA6925A005CFA82 /* Animation.swift */, ); path = Enums; sourceTree = ""; @@ -218,7 +246,7 @@ 4BF6289A2BA2B38D0093637E /* Protocols */ = { isa = PBXGroup; children = ( - 4BF628992BA2B38D0093637E /* Protocols.swift */, + 4BF628992BA2B38D0093637E /* SliderDelegate.swift */, ); path = Protocols; sourceTree = ""; @@ -233,8 +261,9 @@ 4BF6289B2BA2B38D0093637E /* Info.plist */, 4BF6289C2BA2B38D0093637E /* Slider.h */, 4BF6289D2BA2B38D0093637E /* Slider.swift */, - 4BF6289E2BA2B38D0093637E /* SliderTextLayer.swift */, 4BF6289F2BA2B38D0093637E /* SliderTrackLayer.swift */, + 4B8DAEA22BA6ACFF005CFA82 /* ThumbLayer.swift */, + 4B8DAEA52BA6B445005CFA82 /* TextLayer.swift */, ); path = Sources; sourceTree = ""; @@ -358,13 +387,18 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 4BF628A52BA2B38D0093637E /* Protocols.swift in Sources */, + 4BF628A52BA2B38D0093637E /* SliderDelegate.swift in Sources */, 4BF628A32BA2B38D0093637E /* Slider+UITouch.swift in Sources */, - 4BF628A22BA2B38D0093637E /* DirectionEnum.swift in Sources */, + 4BF628A22BA2B38D0093637E /* Direction.swift in Sources */, + 4B8DAE9C2BA69A69005CFA82 /* RangeEndpointsConfiguration.swift in Sources */, 4BF628A42BA2B38D0093637E /* String.swift in Sources */, - 4BF628A92BA2B38D0093637E /* SliderTextLayer.swift in Sources */, + 4B8DAE962BA6925A005CFA82 /* Animation.swift in Sources */, + 4B8DAE9A2BA696A8005CFA82 /* ThumbConfiguration.swift in Sources */, 4BF628A82BA2B38D0093637E /* Slider.swift in Sources */, + 4B8DAEA62BA6B445005CFA82 /* TextLayer.swift in Sources */, 4BF628AA2BA2B38D0093637E /* SliderTrackLayer.swift in Sources */, + 4B8DAE982BA6956B005CFA82 /* TrackConfiguration.swift in Sources */, + 4B8DAEA42BA6AE54005CFA82 /* ThumbLayer.swift in Sources */, 4BF628A12BA2B38D0093637E /* HapticConfiguration.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -373,6 +407,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 4B8DAE9F2BA6A056005CFA82 /* PreferenceManager.swift in Sources */, + 4B8DAEA12BA6A5E9005CFA82 /* CodeViewController+UIColorPickerViewControllerDelegate.swift in Sources */, 4B889B692433375D002FCE02 /* CodeViewController.swift in Sources */, 4B2EB6E523C1E5AA00FB91AD /* AppDelegate.swift in Sources */, 4B889B7624333E7C002FCE02 /* CodeViewController+SliderDelegate.swift in Sources */, diff --git a/Slider.xcodeproj/project.xcworkspace/xcuserdata/ramizkichibekov.xcuserdatad/UserInterfaceState.xcuserstate b/Slider.xcodeproj/project.xcworkspace/xcuserdata/ramizkichibekov.xcuserdatad/UserInterfaceState.xcuserstate index 86b797a..c64492d 100644 Binary files a/Slider.xcodeproj/project.xcworkspace/xcuserdata/ramizkichibekov.xcuserdatad/UserInterfaceState.xcuserstate and b/Slider.xcodeproj/project.xcworkspace/xcuserdata/ramizkichibekov.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/Slider_Example/Application/AppDelegate.swift b/Slider_Example/Application/AppDelegate.swift index 75ca3d7..04c6ffa 100644 --- a/Slider_Example/Application/AppDelegate.swift +++ b/Slider_Example/Application/AppDelegate.swift @@ -1,37 +1,47 @@ // // AppDelegate.swift -// Slider_Example // -// Created by Рамиз Кичибеков on 05.01.2020. -// Copyright © 2020 Ramiz Kichibekov. All rights reserved. +// Copyright (c) 2024 Ramiz Kichibekov (https://github.com/ramiz69) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. // import UIKit @UIApplicationMain -class AppDelegate: UIResponder, UIApplicationDelegate { - +final class AppDelegate: UIResponder, UIApplicationDelegate { - - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - // Override point for customization after application launch. + func application(_ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { return true } // MARK: UISceneSession Lifecycle - func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { - // Called when a new scene session is being created. - // Use this method to select a configuration to create the new scene with. + func application(_ application: UIApplication, + configurationForConnecting connectingSceneSession: UISceneSession, + options: UIScene.ConnectionOptions) -> UISceneConfiguration { return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) } - func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { - // Called when the user discards a scene session. - // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. - // Use this method to release any resources that were specific to the discarded scenes, as they will not return. + func application(_ application: UIApplication, + didDiscardSceneSessions sceneSessions: Set) { } - - + } - diff --git a/Slider_Example/Application/SceneDelegate.swift b/Slider_Example/Application/SceneDelegate.swift index 8277e42..09635fc 100644 --- a/Slider_Example/Application/SceneDelegate.swift +++ b/Slider_Example/Application/SceneDelegate.swift @@ -1,53 +1,35 @@ // // SceneDelegate.swift -// Slider_Example // -// Created by Рамиз Кичибеков on 05.01.2020. -// Copyright © 2020 Ramiz Kichibekov. All rights reserved. +// Copyright (c) 2024 Ramiz Kichibekov (https://github.com/ramiz69) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. // import UIKit -class SceneDelegate: UIResponder, UIWindowSceneDelegate { +final class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? - func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { - // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. - // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. - // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). - guard let _ = (scene as? UIWindowScene) else { return } - } - - func sceneDidDisconnect(_ scene: UIScene) { - // Called as the scene is being released by the system. - // This occurs shortly after the scene enters the background, or when its session is discarded. - // Release any resources associated with this scene that can be re-created the next time the scene connects. - // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). - } - - func sceneDidBecomeActive(_ scene: UIScene) { - // Called when the scene has moved from an inactive state to an active state. - // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. + } - func sceneWillResignActive(_ scene: UIScene) { - // Called when the scene will move from an active state to an inactive state. - // This may occur due to temporary interruptions (ex. an incoming phone call). - } - - func sceneWillEnterForeground(_ scene: UIScene) { - // Called as the scene transitions from the background to the foreground. - // Use this method to undo the changes made on entering the background. - } - - func sceneDidEnterBackground(_ scene: UIScene) { - // Called as the scene transitions from the foreground to the background. - // Use this method to save data, release shared resources, and store enough scene-specific state information - // to restore the scene back to its current state. - } - - } - diff --git a/Slider_Example/Flows/Main/Controllers/CodeViewController.swift b/Slider_Example/Flows/Main/Controllers/CodeViewController.swift index 813dd52..84eacf2 100644 --- a/Slider_Example/Flows/Main/Controllers/CodeViewController.swift +++ b/Slider_Example/Flows/Main/Controllers/CodeViewController.swift @@ -1,9 +1,25 @@ // // CodeViewController.swift -// Slider_Example // -// Created by Рамиз Кичибеков on 31.03.2020. -// Copyright © 2020 Ramiz Kichibekov. All rights reserved. +// Copyright (c) 2024 Ramiz Kichibekov (https://github.com/ramiz69) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. // import UIKit @@ -14,13 +30,30 @@ final class CodeViewController: UIViewController { // MARK: - Outlets @IBOutlet var segmentControl: UISegmentedControl! - @IBOutlet var reachSwitch: UISwitch! - @IBOutlet var valueSwitch: UISwitch! - @IBOutlet var directionSwitch: UISwitch! + @IBOutlet var preferenceBarButton: UIBarButtonItem! // MARK: - Properties + enum ColorPickerType { + case thumb + case track(Track) + case endpoint(Endpoint) + + enum Endpoint { + case minimum + case maximum + } + + enum Track { + case min + case max + case reverseMin + } + } + private let slider = Slider() + private(set) var preference = PreferenceManager() + private(set) var selectedColorPickerType: ColorPickerType! // MARK: - Life cycle @@ -28,17 +61,55 @@ final class CodeViewController: UIViewController { super.viewDidLoad() configureController() + configurePreferenceMenu() + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + // let offset: CGFloat = 16 + // slider.frame = CGRect(x: offset, + // y: view.safeAreaInsets.top, + // width: view.bounds.width - 2 * offset, + // height: slider.trackHeight) + } + + // MARK: Public methods + + func configureMinimumEndpoint() { + slider.minimumEndpointConfiguration = .init(foregroundColor: preference.minimumEndpointPreference.foregroundColor, + aligmentMode: preference.minimumEndpointPreference.aligmentMode) + configurePreferenceMenu() + } + + func configureMaximumEndpoint() { + slider.maximumEndpointConfiguration = .init(foregroundColor: preference.maximumEndpointPreference.foregroundColor, + aligmentMode: preference.maximumEndpointPreference.aligmentMode) + configurePreferenceMenu() + } + + func configureThumb() { + slider.thumbConfiguration = .init(backgroundColor: preference.thumbPreference.backgroundColor) + configurePreferenceMenu() + } + + func configureTrack() { + slider.trackConfiguration = .init(maxColor: preference.trackPreference.maxColor, + minColor: preference.trackPreference.minColor, + reverseMinColor: preference.trackPreference.reverseMinColor) + configurePreferenceMenu() } // MARK: Private methods private func configureController() { - // slider.delegate = self - slider.direction = DirectionEnum(withValue: segmentControl.selectedSegmentIndex) +// slider.delegate = self slider.maximum = 1500 slider.minimum = .zero slider.value = .zero + slider.animationStyle = .default view.addSubview(slider) + slider.translatesAutoresizingMaskIntoConstraints = false let layoutMarginsGuide = view.layoutMarginsGuide let offset: CGFloat = 16 let constraints = [slider.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor, constant: offset), @@ -48,29 +119,169 @@ final class CodeViewController: UIViewController { } private func configureHaptic() { - slider.hapticConfiguration = .init(reachLimitValueHapticEnabled: reachSwitch.isOn, - changeValueHapticEnabled: valueSwitch.isOn, - changeDirectionHapticEnabled: directionSwitch.isOn, + slider.hapticConfiguration = .init(reachLimitValueHapticEnabled: preference.hapticPreference.reachMinMaxValues, + changeValueHapticEnabled: preference.hapticPreference.valueChanges, + changeDirectionHapticEnabled: preference.hapticPreference.directionChanges, reachImpactGeneratorStyle: .medium, changeValueImpactGeneratorStyle: .light) + configurePreferenceMenu() } - // MARK: - Actions + private func resetSlider() { + slider.hapticConfiguration = .init(reachLimitValueHapticEnabled: preference.hapticPreference.reachMinMaxValues, + changeValueHapticEnabled: preference.hapticPreference.valueChanges, + changeDirectionHapticEnabled: preference.hapticPreference.directionChanges, + reachImpactGeneratorStyle: .medium, + changeValueImpactGeneratorStyle: .light) + slider.thumbConfiguration = .init() + slider.maximumEndpointConfiguration = .init() + slider.minimumEndpointConfiguration = .init() + } - @IBAction private func didChangeSegment(_ sender: UISegmentedControl) { - slider.direction = DirectionEnum(withValue: sender.selectedSegmentIndex) + private func configurePreferenceMenu() { + let reset = UIAction(title: "Reset") { [weak self] _ in + self?.preference = PreferenceManager.reset() + self?.resetSlider() + } + let menu = UIMenu(title: "Preference", + image: UIImage(systemName: "gear"), + children: [configureAnimationMenu(), + configureTrackMenu(), + configureHapticMenu(), + configureEndpointMenu(), + configureThumbMenu(), + reset]) + + preferenceBarButton.menu = menu } - @IBAction private func didChangeReachValue(_ sender: UISwitch) { - configureHaptic() + private func configureAnimationMenu() -> UIMenu { + let styles: [AnimationStyle] = [.none, .default] + var animations = [UIAction]() + styles.forEach { style in + let action = UIAction(title: "\(style)", state: style == slider.animationStyle ? .on : .off) { [weak self] _ in + self?.slider.animationStyle = style + self?.configurePreferenceMenu() + } + animations.append(action) + } + + return UIMenu(title: "Animation Direction Change", children: animations) } - @IBAction private func didChangeValue(_ sender: UISwitch) { - configureHaptic() + private func configureTrackMenu() -> UIMenu { + let minColorAction = UIAction(title: "Minimum Color") { [weak self] _ in + self?.selectedColorPickerType = .track(.min) + self?.presentColorPicker() + } + let maxColorAction = UIAction(title: "Maximum Color") { [weak self] _ in + self?.selectedColorPickerType = .track(.max) + self?.presentColorPicker() + } + let reverseMinColorAction = UIAction(title: "Reverse Minimum Color") { [weak self] _ in + self?.selectedColorPickerType = .track(.reverseMin) + self?.presentColorPicker() + } + return UIMenu(title: "Track", children: [minColorAction, maxColorAction, reverseMinColorAction]) } - @IBAction private func didChangeDirectionValue(_ sender: UISwitch) { - configureHaptic() + private func configureThumbMenu() -> UIMenu { + let backgroundColor = UIAction(title: "Background") { [weak self] _ in + self?.selectedColorPickerType = .thumb + self?.presentColorPicker() + } + + return UIMenu(title: "Thumb", children: [backgroundColor]) } + private func configureHapticMenu() -> UIMenu { + let hapticPreference = preference.hapticPreference + let reachAction = UIAction(title: "Reach Min/Max values", + state: hapticPreference.reachMinMaxValues ? .on : .off) { [weak self] _ in + hapticPreference.reachMinMaxValues.toggle() + self?.configureHaptic() + } + let valueChangeAction = UIAction(title: "Value changes", + state: hapticPreference.valueChanges ? .on : .off) { [weak self] _ in + hapticPreference.valueChanges.toggle() + self?.configureHaptic() + } + let directionChangeAction = UIAction(title: "Direction changes", + state: hapticPreference.directionChanges ? .on : .off) { [weak self] _ in + hapticPreference.directionChanges.toggle() + self?.configureHaptic() + } + + return UIMenu(title: "Haptic", + image: UIImage(systemName: "iphone.gen3.radiowaves.left.and.right"), + children: [reachAction, valueChangeAction, directionChangeAction]) + } + + private func configureEndpointMenu() -> UIMenu { + let maximumEndpointPreference = preference.maximumEndpointPreference + let maximumTextColorAction = UIAction(title: "Change color") { [weak self] _ in + self?.selectedColorPickerType = .endpoint(.maximum) + self?.presentColorPicker() + } + + let minimumEndpointPreference = preference.minimumEndpointPreference + let minimumTextColorAction = UIAction(title: "Change color") { [weak self] _ in + self?.selectedColorPickerType = .endpoint(.minimum) + self?.presentColorPicker() + } + + + let minimumAligmentMenu = UIMenu(title: "Aligment", + image: UIImage(systemName: "text.alignleft"), + children: configureAligmentActions(for: minimumEndpointPreference)) + let maximumAligmentMenu = UIMenu(title: "Aligment", + image: UIImage(systemName: "text.alignright"), + children: configureAligmentActions(for: maximumEndpointPreference)) + + let minimumEndpointMenu = UIMenu(title: "Minimum", children: [minimumTextColorAction, minimumAligmentMenu]) + let maximumEndpointMenu = UIMenu(title: "Maximum", children: [maximumTextColorAction, maximumAligmentMenu]) + + return UIMenu(title: "Endpoint", + image: UIImage(systemName: "filemenu.and.selection"), + children: [minimumEndpointMenu, maximumEndpointMenu]) + } + + private func configureAligmentActions(for endpoint: EndpointPreference) -> [UIAction] { + var aligmentActions = [UIAction]() + let aligments = CATextLayerAlignmentMode.allCases + aligments.forEach { aligment in + let action = UIAction(title: aligment.rawValue, + state: aligment.rawValue == endpoint.aligmentMode.rawValue ? .on : .off) { [weak self] _ in + endpoint.aligmentMode = aligment + self?.configureMinimumEndpoint() + } + aligmentActions.append(action) + } + + return aligmentActions + } + + private func presentColorPicker() { + let colorPicker = UIColorPickerViewController() + colorPicker.delegate = self + present(colorPicker, animated: true) + } + + // MARK: - Actions + + @IBAction private func didChangeSegment(_ sender: UISegmentedControl) { + switch sender.selectedSegmentIndex { + case 1: + slider.direction = .rightToLeft + default: + slider.direction = .leftToRight + } + } + +} + +extension CATextLayerAlignmentMode: CaseIterable { + public static var allCases: [CATextLayerAlignmentMode] { + [.left, .natural, .center, .right, .justified] + } } diff --git a/Slider_Example/Flows/Main/Extensions/CodeViewController+SliderDelegate.swift b/Slider_Example/Flows/Main/Extensions/CodeViewController+SliderDelegate.swift index bc26707..f277b77 100644 --- a/Slider_Example/Flows/Main/Extensions/CodeViewController+SliderDelegate.swift +++ b/Slider_Example/Flows/Main/Extensions/CodeViewController+SliderDelegate.swift @@ -1,9 +1,25 @@ // // CodeViewController+SliderDelegate.swift -// Slider_Example // -// Created by Рамиз Кичибеков on 31.03.2020. -// Copyright © 2020 Ramiz Kichibekov. All rights reserved. +// Copyright (c) 2024 Ramiz Kichibekov (https://github.com/ramiz69) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. // import UIKit diff --git a/Slider_Example/Flows/Main/Extensions/CodeViewController+UIColorPickerViewControllerDelegate.swift b/Slider_Example/Flows/Main/Extensions/CodeViewController+UIColorPickerViewControllerDelegate.swift new file mode 100644 index 0000000..22c59c0 --- /dev/null +++ b/Slider_Example/Flows/Main/Extensions/CodeViewController+UIColorPickerViewControllerDelegate.swift @@ -0,0 +1,60 @@ +// +// CodeViewController+UIColorPickerViewControllerDelegate.swift +// +// Copyright (c) 2024 Ramiz Kichibekov (https://github.com/ramiz69) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import UIKit + +extension CodeViewController: UIColorPickerViewControllerDelegate { + + func colorPickerViewControllerDidSelectColor(_ viewController: UIColorPickerViewController) { + let selectedColor = viewController.selectedColor + switch selectedColorPickerType { + case .endpoint(let endpoint): + if endpoint == .minimum { + preference.minimumEndpointPreference.foregroundColor = selectedColor.cgColor + configureMinimumEndpoint() + } else { + preference.maximumEndpointPreference.foregroundColor = selectedColor.cgColor + configureMaximumEndpoint() + } + case .thumb: + preference.thumbPreference.backgroundColor = selectedColor + configureThumb() + case .track(let track): + if track == .max { + preference.trackPreference.maxColor = selectedColor + } else if track == .min { + preference.trackPreference.minColor = selectedColor + } else { + preference.trackPreference.reverseMinColor = selectedColor + } + configureTrack() + default: break + } + } + + func colorPickerViewControllerDidFinish(_ viewController: UIColorPickerViewController) { + viewController.dismiss(animated: true) + } + +} diff --git a/Slider_Example/Flows/Main/View/Base.lproj/Main.storyboard b/Slider_Example/Flows/Main/View/Base.lproj/Main.storyboard index 93969e6..cc43c8c 100644 --- a/Slider_Example/Flows/Main/View/Base.lproj/Main.storyboard +++ b/Slider_Example/Flows/Main/View/Base.lproj/Main.storyboard @@ -5,55 +5,9 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -61,85 +15,12 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + @@ -149,17 +30,16 @@ + - - + - - + @@ -172,7 +52,7 @@ - + @@ -181,8 +61,6 @@ - - - + diff --git a/Slider_Example/Manager/PreferenceManager.swift b/Slider_Example/Manager/PreferenceManager.swift new file mode 100644 index 0000000..e502af3 --- /dev/null +++ b/Slider_Example/Manager/PreferenceManager.swift @@ -0,0 +1,119 @@ +// +// PreferenceManager.swift +// +// Copyright (c) 2024 Ramiz Kichibekov (https://github.com/ramiz69) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import UIKit +import QuartzCore + +final class PreferenceManager { + + let thumbPreference: ThumbPreference + let trackPreference: TrackPreference + let minimumEndpointPreference: EndpointPreference + let maximumEndpointPreference: EndpointPreference + let hapticPreference: HapticPreference + + init(thumbPreference: ThumbPreference = ThumbPreference(), + trackPreference: TrackPreference = TrackPreference(), + minimumEndpointPreference: EndpointPreference = EndpointPreference(), + maximumEndpointPreference: EndpointPreference = EndpointPreference(), + hapticPreference: HapticPreference = HapticPreference()) { + self.thumbPreference = thumbPreference + self.trackPreference = trackPreference + self.minimumEndpointPreference = minimumEndpointPreference + self.maximumEndpointPreference = maximumEndpointPreference + self.hapticPreference = hapticPreference + } + + class func reset() -> PreferenceManager { + .init() + } + +} + +final class HapticPreference { + + var reachMinMaxValues = true + var valueChanges = true + var directionChanges = true + + init(reachMinMaxValues: Bool = true, + valueChanges: Bool = true, + directionChanges: Bool = true) { + self.reachMinMaxValues = reachMinMaxValues + self.valueChanges = valueChanges + self.directionChanges = directionChanges + } + +} + +final class EndpointPreference { + var foregroundColor: CGColor + var aligmentMode: CATextLayerAlignmentMode + + init(foregroundColor: CGColor = CGColor(red: 1, green: 1, blue: 1, alpha: 1), + aligmentMode: CATextLayerAlignmentMode = .center) { + self.foregroundColor = foregroundColor + self.aligmentMode = aligmentMode + } +} + +final class TrackPreference { + var maxColor: UIColor + var minColor: UIColor + var reverseMinColor: UIColor + + init(maxColor: UIColor = UIColor(red: 191 / 255, + green: 194 / 255, + blue: 209 / 255, + alpha: 1), + minColor: UIColor = UIColor(red: .zero, + green: 122 / 255, + blue: 1, + alpha: 1), + reverseMinColor: UIColor = UIColor(red: 247 / 255, + green: 73 / 255, + blue: 2 / 255, + alpha: 1)) { + self.maxColor = maxColor + self.minColor = minColor + self.reverseMinColor = reverseMinColor + } +} + +final class ThumbPreference { + var backgroundColor: UIColor = .white + var textColor: UIColor = UIColor(red: .zero, + green: 74 / 255, + blue: 150 / 255, + alpha: 1) + + init(backgroundColor: UIColor = .white, + textColor: UIColor = UIColor(red: .zero, + green: 74 / 255, + blue: 150 / 255, + alpha: 1)) { + self.backgroundColor = backgroundColor + self.textColor = textColor + } +} diff --git a/Sources/Configurations/RangeEndpointsConfiguration.swift b/Sources/Configurations/RangeEndpointsConfiguration.swift new file mode 100644 index 0000000..a382e3d --- /dev/null +++ b/Sources/Configurations/RangeEndpointsConfiguration.swift @@ -0,0 +1,44 @@ +// +// ThumbConfiguration.swift +// +// Copyright (c) 2024 Ramiz Kichibekov (https://github.com/ramiz69) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation +import QuartzCore + +public struct RangeEndpointsConfiguration { + + let anchorPoint: CGPoint + let foregroundColor: CGColor + let fontSize: CGFloat + let aligmentMode: CATextLayerAlignmentMode + + public init(anchorPoint: CGPoint = CGPoint(x: 0.5, y: 0.5), + foregroundColor: CGColor = CGColor(red: 1, green: 1, blue: 1, alpha: 1), + fontSize: CGFloat = 12, + aligmentMode: CATextLayerAlignmentMode = .center) { + self.anchorPoint = anchorPoint + self.foregroundColor = foregroundColor + self.fontSize = fontSize + self.aligmentMode = aligmentMode + } +} diff --git a/Sources/Configurations/ThumbConfiguration.swift b/Sources/Configurations/ThumbConfiguration.swift new file mode 100644 index 0000000..052a0df --- /dev/null +++ b/Sources/Configurations/ThumbConfiguration.swift @@ -0,0 +1,41 @@ +// +// ThumbConfiguration.swift +// +// Copyright (c) 2024 Ramiz Kichibekov (https://github.com/ramiz69) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import UIKit + +public struct ThumbConfiguration { + + public var backgroundColor: UIColor + public var fontSize: CGFloat + public var size: CGSize + + public init(backgroundColor: UIColor = .white, + fontSize: CGFloat = 14, + size: CGSize = CGSize(width: 60, height: 36)) { + self.backgroundColor = backgroundColor + self.fontSize = fontSize + self.size = size + } + +} diff --git a/Sources/Configurations/TrackConfiguration.swift b/Sources/Configurations/TrackConfiguration.swift new file mode 100644 index 0000000..8ecf6d4 --- /dev/null +++ b/Sources/Configurations/TrackConfiguration.swift @@ -0,0 +1,56 @@ +// +// TrackConfiguration.swift +// +// Copyright (c) 2024 Ramiz Kichibekov (https://github.com/ramiz69) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import UIKit + +public struct TrackConfiguration { + + public var maxColor: UIColor + public var minColor: UIColor + public var reverseMinColor: UIColor + public var height: CGFloat + public var inset: CGFloat + + public init(maxColor: UIColor = UIColor(red: 191 / 255, + green: 194 / 255, + blue: 209 / 255, + alpha: 1), + minColor: UIColor = UIColor(red: .zero, + green: 122 / 255, + blue: 1, + alpha: 1), + reverseMinColor: UIColor = UIColor(red: 247 / 255, + green: 73 / 255, + blue: 2 / 255, + alpha: 1), + height: CGFloat = 36, + inset: CGFloat = .zero) { + self.maxColor = maxColor + self.minColor = minColor + self.reverseMinColor = reverseMinColor + self.height = height + self.inset = inset + } + +} diff --git a/Sources/Enums/Animation.swift b/Sources/Enums/Animation.swift new file mode 100644 index 0000000..82b3339 --- /dev/null +++ b/Sources/Enums/Animation.swift @@ -0,0 +1,38 @@ +// +// Animation.swift +// +// Copyright (c) 2024 Ramiz Kichibekov (https://github.com/ramiz69) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation +import QuartzCore + +public enum AnimationStyle { + case none + case `default` + + var animationDuration: TimeInterval { + switch self { + case .none: .zero + case .default: CATransaction.animationDuration() + } + } +} diff --git a/Sources/Enums/DirectionEnum.swift b/Sources/Enums/Direction.swift similarity index 79% rename from Sources/Enums/DirectionEnum.swift rename to Sources/Enums/Direction.swift index 9b60637..863d90f 100644 --- a/Sources/Enums/DirectionEnum.swift +++ b/Sources/Enums/Direction.swift @@ -1,7 +1,7 @@ // -// DirectionEnum.swift +// Direction.swift // -// Copyright (c) 2020 Ramiz Kichibekov (https://github.com/ramiz69) +// Copyright (c) 2024 Ramiz Kichibekov (https://github.com/ramiz69) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -22,17 +22,8 @@ // THE SOFTWARE. // -import UIKit +import Foundation -public enum DirectionEnum { - +public enum Direction { case leftToRight, rightToLeft - - public init(withValue value: Int) { - switch value { - case .zero: self = .leftToRight - default: self = .rightToLeft - } - } - } diff --git a/Sources/Extensions/Slider+UITouch.swift b/Sources/Extensions/Slider+UITouch.swift index 2d046ed..a4913c1 100644 --- a/Sources/Extensions/Slider+UITouch.swift +++ b/Sources/Extensions/Slider+UITouch.swift @@ -29,6 +29,7 @@ extension Slider { public override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool { previousTouchPoint = touch.location(in: self) if thumbLayer.frame.contains(previousTouchPoint) { + didBeginTracking() delegate?.didBeginTracking(self) return true @@ -39,10 +40,16 @@ extension Slider { public override func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool { let touchPoint = touch.location(in: self) - let delta = (touchPoint.x - previousTouchPoint.x).rounded(.toNearestOrEven) - let ratio = delta / usableTrackingLength - let valueDelta = ((maximum - minimum) * ratio).rounded(.toNearestOrEven) - let tempValue = value + valueDelta + let deltaLocation = (touchPoint.x - previousTouchPoint.x).rounded(.toNearestOrEven) + let ratio = deltaLocation / usableTrackingLength + let deltaValue = ((maximum - minimum) * ratio).rounded(.toNearestOrEven) + let tempValue: CGFloat + switch direction { + case .leftToRight: + tempValue = value + deltaValue + case .rightToLeft: + tempValue = value - deltaValue + } let noOfStep = (tempValue / step).rounded(.toNearestOrEven) var currentValue = noOfStep * step if (currentValue == maximum || currentValue == minimum) && currentValue != value { @@ -56,13 +63,11 @@ extension Slider { if currentValue == value { return true } + value = currentValue hapticConfiguration.valueGenerate() previousTouchPoint = touchPoint - delegate?.didContinueTracking(self) - if continuous { - sendActions(for: .valueChanged) - } + sendActions(for: .valueChanged) return true } @@ -70,6 +75,7 @@ extension Slider { public override func endTracking(_ touch: UITouch?, with event: UIEvent?) { super.endTracking(touch, with: event) + endTracking() if step > .zero { let noOfStep = (value / step).rounded(.toNearestOrEven) value = noOfStep * step diff --git a/Sources/Protocols/Protocols.swift b/Sources/Protocols/SliderDelegate.swift similarity index 94% rename from Sources/Protocols/Protocols.swift rename to Sources/Protocols/SliderDelegate.swift index bb93f71..1372bd2 100644 --- a/Sources/Protocols/Protocols.swift +++ b/Sources/Protocols/SliderDelegate.swift @@ -1,5 +1,5 @@ // -// Protocols.swift +// SliderDelegate.swift // // Copyright (c) 2020 Ramiz Kichibekov (https://github.com/ramiz69) // @@ -21,8 +21,11 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // -import UIKit +import Foundation +import CoreFoundation + +@MainActor public protocol SliderDelegate: AnyObject { func slider(_ slider: Slider, displayTextForValue value: CGFloat) -> String func didBeginTracking(_ slider: Slider) diff --git a/Sources/Slider.swift b/Sources/Slider.swift index e982b51..71e1652 100644 --- a/Sources/Slider.swift +++ b/Sources/Slider.swift @@ -23,6 +23,7 @@ // import UIKit +import QuartzCore open class Slider: UIControl { @@ -31,23 +32,21 @@ open class Slider: UIControl { if let maxText: String = delegate?.slider(self, displayTextForValue: maximum), !maxText.isEmpty { - let newWidth = maxText.size(withConstrainedWidth: thumbHeight, - font: UIFont.boldSystemFont(ofSize: fontSize)).width - thumbWidth = CGFloat(max(newWidth, thumbWidth)) - thumbLayer.bounds = CGRect(origin: .zero, - size: CGSize(width: thumbWidth, - height: thumbHeight)) - thumbLayer.position = CGPoint(x: positionForValue(value: minimum), - y: bounds.height / 2) - thumbLayer.cornerRadius = thumbHeight / 2 + let thumbSize = thumbConfiguration.size + let newWidth = maxText.size(withConstrainedWidth: thumbSize.height, + font: UIFont.boldSystemFont(ofSize: thumbConfiguration.fontSize)).width + thumbConfiguration.size.width = CGFloat(max(newWidth, thumbSize.width)) + thumbLayer.bounds = CGRect(origin: .zero, size: thumbSize) + thumbLayer.position = positionForValue(value: minimum) + thumbLayer.cornerRadius = thumbSize.height / 2 thumbLayer.shadowPath = UIBezierPath(roundedRect: thumbLayer.bounds, - cornerRadius: thumbHeight / 2).cgPath + cornerRadius: thumbLayer.cornerRadius).cgPath } updateThumbLayersText() } } - // MARK: - Customization properties + // MARK: Customization properties final public var value: CGFloat = 10 { didSet { @@ -58,7 +57,7 @@ open class Slider: UIControl { value = minimum } reinitComponentValues() - redrawLayers() + setNeedsLayersDisplay() } } @@ -68,7 +67,7 @@ open class Slider: UIControl { maximum = minimum } reinitComponentValues() - redrawLayers() + setNeedsLayersDisplay() } } @@ -81,7 +80,7 @@ open class Slider: UIControl { value = maximum } reinitComponentValues() - redrawLayers() + setNeedsLayersDisplay() } } @@ -99,172 +98,117 @@ open class Slider: UIControl { final public var cornerRadius: CGFloat = 16 { didSet { reinitComponentValues() - redrawLayers() - } - } - - final public var thumbBackgroundColor: UIColor = UIColor.white { - didSet { - thumbLayer.backgroundColor = thumbBackgroundColor.cgColor - redrawLayers() - } - } - - final public var thumbTextColor: UIColor = UIColor(red: .zero, - green: 74 / 255, - blue: 150 / 255, - alpha: 1) { - didSet { - thumbLayer.foregroundColor = thumbTextColor.cgColor - redrawLayers() + setNeedsLayersDisplay() } } - final public var continuous: Bool = true - - final public var fontSize: CGFloat = 14 { + public var thumbConfiguration = ThumbConfiguration() { didSet { - thumbLayer.setNeedsDisplay() + updateThumbLayersText() + setNeedsLayersDisplay() } } - final public var trackHeight: CGFloat = 36 { + public var trackConfiguration = TrackConfiguration() { didSet { - layoutSubviews() + reinitComponentValues() + setNeedsLayersDisplay() } } - final public var trackInset: CGFloat = .zero { + public var maximumEndpointConfiguration = RangeEndpointsConfiguration() { didSet { - layoutSubviews() + initEndpointLayer(.maximum) } } - final public var thumbHeight: CGFloat = 36 { + public var minimumEndpointConfiguration = RangeEndpointsConfiguration() { didSet { - initThumbLayer() - layoutSubviews() - redrawLayers() + initEndpointLayer(.minimum) } } - final public var thumbWidth: CGFloat = 60 { - didSet { - initThumbLayer() - layoutSubviews() - redrawLayers() - } - } - - open var trackMaxColor: UIColor = UIColor(red: 191 / 255, - green: 194 / 255, - blue: 209 / 255, - alpha: 1) { - didSet { - reinitComponentValues() - redrawLayers() - } - } + public var hapticConfiguration: HapticConfiguration = .init(reachLimitValueHapticEnabled: true, + changeValueHapticEnabled: false, + changeDirectionHapticEnabled: true, + reachImpactGeneratorStyle: .medium, + changeValueImpactGeneratorStyle: .light) - open var trackMinColor: UIColor = UIColor(red: .zero, - green: 122 / 255, - blue: 1, - alpha: 1) { - didSet { - reinitComponentValues() - redrawLayers() - } - } + // MARK: Properties - open var reverseTrackMinColor: UIColor = UIColor(red: 247 / 255, - green: 73 / 255, - blue: 2 / 255, - alpha: 1) { - didSet { - reinitComponentValues() - redrawLayers() - } + open override var intrinsicContentSize: CGSize { + CGSize(width: UIView.noIntrinsicMetric, height: trackConfiguration.height) } - // MARK: - Properties - - final public var direction: DirectionEnum = .leftToRight { + final public var direction: Direction = .leftToRight { didSet { hapticConfiguration.directionGenerate() - CATransaction.begin() - CATransaction.setDisableActions(true) - CATransaction.setAnimationDuration(.zero) - let affineTransform = CATransform3DMakeAffineTransform(direction == .leftToRight ? .identity : CGAffineTransform.identity.rotated(by: .pi)) - transform = direction == .leftToRight ? .identity : CGAffineTransform.identity.rotated(by: .pi) - thumbLayer.transform = affineTransform - minimumLayer.transform = affineTransform - maximumLayer.transform = affineTransform - CATransaction.commit() - reinitComponentValues() redrawLayers() + reinitComponentValues() + setNeedsLayersDisplay() } } - - public var hapticConfiguration: HapticConfiguration = .init(reachLimitValueHapticEnabled: true, - changeValueHapticEnabled: false, - changeDirectionHapticEnabled: true, - reachImpactGeneratorStyle: .medium, - changeValueImpactGeneratorStyle: .light) - public let trackLayer = SliderTrackLayer() - public let thumbLayer = SliderTextLayer() - public let minimumLayer = SliderMinimumTextLayer() - public let maximumLayer = SliderMaximumTextLayer() final public var previousTouchPoint: CGPoint = .zero final public var usableTrackingLength: CGFloat = .zero + final public var animationStyle: AnimationStyle = .none + final public var continuous: Bool = true - open override var intrinsicContentSize: CGSize { - CGSize(width: UIView.noIntrinsicMetric, height: trackHeight) - } + let thumbLayer = ThumbLayer() + private let trackLayer = SliderTrackLayer() + private let minimumLayer = TextLayer() + private let maximumLayer = TextLayer() + + private var displayLink: CADisplayLink? + private var isDirectionChangeAnimationInProgress = false - private let trackMaskLayer = CALayer() + private enum Endpoint: CaseIterable { + case minimum, maximum + } - // MARK: - Initial methods + // MARK: Initial methods - required public override init(frame: CGRect) { + public override init(frame: CGRect) { super.init(frame: frame) initialControl() initLayers() - commonInit() } @available(*, unavailable) - required public init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") } - // MARK: - Life cycle + // MARK: Life cycle public override func layoutSubviews() { super.layoutSubviews() - trackLayer.frame = trackRectForBound(bounds) - trackMaskLayer.masksToBounds = true - trackMaskLayer.frame = trackRectForBound(bounds) - trackMaskLayer.cornerRadius = cornerRadius - invalidateIntrinsicContentSize() - commonInit() - updateThumbLayersPosition() - redrawLayers() + if !isDirectionChangeAnimationInProgress { + usableTrackingLength = bounds.width - thumbConfiguration.size.width + trackLayer.frame = trackRectForBounds() + minimumLayer.position = positionForValue(value: minimum) + maximumLayer.position = positionForValue(value: maximum) + updateSlider() + } } - public override func prepareForInterfaceBuilder() { - trackLayer.frame = trackRectForBound(bounds) - commonInit() - updateThumbLayersPosition() - redrawLayers() + // MARK: Public methods + + func didBeginTracking() { + displayLink = CADisplayLink(target: self, selector: #selector(updateSlider)) + displayLink?.add(to: .current, forMode: .common) } - // MARK: - Private methods + func endTracking() { + displayLink?.invalidate() + displayLink = nil + } + + // MARK: Private methods private func initialControl() { backgroundColor = .clear - translatesAutoresizingMaskIntoConstraints = false layer.addSublayer(trackLayer) layer.addSublayer(minimumLayer) layer.addSublayer(maximumLayer) @@ -272,137 +216,165 @@ open class Slider: UIControl { } private func initLayers() { - trackLayer.contentsScale = UIScreen.main.scale - trackLayer.frame = trackRectForBound(bounds) - trackLayer.setNeedsDisplay() - trackMaskLayer.masksToBounds = true - trackMaskLayer.frame = trackRectForBound(bounds) - trackMaskLayer.cornerRadius = cornerRadius - layer.insertSublayer(trackMaskLayer, at: .zero) + trackLayer.contentsScale = getScallingFactor() + trackLayer.frame = trackRectForBounds() reinitComponentValues() initThumbLayer() - initMinimumLayer() - initMaximumLayer() + let endpoints = Endpoint.allCases + endpoints.forEach { initEndpointLayer($0) } updateThumbLayersText() } private func initThumbLayer() { thumbLayer.anchorPoint = CGPoint(x: 0.5, y: 0.5) - thumbLayer.bounds = CGRect(x: .zero, y: .zero, width: thumbWidth, height: thumbHeight) - let xPosition = positionForValue(value: minimum) - thumbLayer.position = CGPoint(x: xPosition, y: bounds.height / 2) - thumbLayer.foregroundColor = trackMinColor.cgColor - thumbLayer.cornerRadius = thumbHeight / 2 - thumbLayer.font = UIFont.systemFont(ofSize: fontSize, weight: .black) - thumbLayer.fontSize = fontSize - thumbLayer.backgroundColor = thumbBackgroundColor.cgColor + thumbLayer.bounds = CGRect(origin: .zero, size: thumbConfiguration.size) + thumbLayer.position = positionForValue(value: minimum) + thumbLayer.foregroundColor = trackConfiguration.minColor.cgColor + thumbLayer.cornerRadius = thumbConfiguration.size.height / 2 + thumbLayer.font = UIFont.systemFont(ofSize: thumbConfiguration.fontSize, weight: .black) + thumbLayer.fontSize = thumbConfiguration.fontSize thumbLayer.alignmentMode = .center - thumbLayer.contentsScale = UIScreen.main.scale + thumbLayer.contentsScale = getScallingFactor() thumbLayer.masksToBounds = false - thumbLayer.shadowOffset = CGSize(width: 0, height: 0.5) + thumbLayer.shadowOffset = CGSize(width: .zero, height: 0.5) thumbLayer.shadowColor = UIColor.black.cgColor thumbLayer.shadowRadius = 2 thumbLayer.shadowOpacity = 0.125 thumbLayer.shadowPath = UIBezierPath(roundedRect: thumbLayer.bounds, - cornerRadius: thumbHeight / 2).cgPath - } - - private func initMinimumLayer() { - minimumLayer.anchorPoint = CGPoint(x: 0.5, y: 0.5) - minimumLayer.bounds = CGRect(x: .zero, y: .zero, width: thumbWidth, height: thumbHeight) - let xPosition = positionForValue(value: minimum) - minimumLayer.position = CGPoint(x: xPosition, y: bounds.height / 2) - minimumLayer.foregroundColor = UIColor.white.cgColor - minimumLayer.fontSize = 12 - minimumLayer.alignmentMode = .center - minimumLayer.contentsScale = UIScreen.main.scale - } - - private func initMaximumLayer() { - maximumLayer.anchorPoint = CGPoint(x: 0.5, y: 0.5) - maximumLayer.bounds = CGRect(x: .zero, y: .zero, width: thumbWidth, height: thumbHeight) - let xPosition = positionForValue(value: maximum) - maximumLayer.position = CGPoint(x: xPosition, y: bounds.height / 2) - maximumLayer.foregroundColor = UIColor.white.cgColor - maximumLayer.fontSize = 12 - maximumLayer.alignmentMode = .center - maximumLayer.contentsScale = UIScreen.main.scale + cornerRadius: thumbLayer.cornerRadius).cgPath + } + + private func initEndpointLayer(_ endpoint: Endpoint) { + let layerFrame = CGRect(origin: .zero, size: thumbConfiguration.size) + let scale = getScallingFactor() + switch endpoint { + case .minimum: + minimumLayer.anchorPoint = minimumEndpointConfiguration.anchorPoint + minimumLayer.bounds = layerFrame + minimumLayer.position = positionForValue(value: minimum) + minimumLayer.foregroundColor = minimumEndpointConfiguration.foregroundColor + minimumLayer.fontSize = minimumEndpointConfiguration.fontSize + minimumLayer.alignmentMode = minimumEndpointConfiguration.aligmentMode + minimumLayer.contentsScale = scale + case .maximum: + maximumLayer.anchorPoint = maximumEndpointConfiguration.anchorPoint + maximumLayer.bounds = layerFrame + maximumLayer.position = positionForValue(value: maximum) + maximumLayer.foregroundColor = maximumEndpointConfiguration.foregroundColor + maximumLayer.fontSize = maximumEndpointConfiguration.fontSize + maximumLayer.alignmentMode = maximumEndpointConfiguration.aligmentMode + maximumLayer.contentsScale = scale + } } - private func commonInit() { - usableTrackingLength = bounds.width - thumbWidth - translatesAutoresizingMaskIntoConstraints = false + private func getScallingFactor() -> CGFloat { + window?.screen.scale ?? UIScreen.main.scale } // MARK: Update methods private func reinitComponentValues() { - trackLayer.minimumValue = minimum - trackLayer.maximumValue = maximum - trackLayer.trackMaxColor = trackMaxColor - trackLayer.trackMinColor = direction == .leftToRight ? trackMinColor : reverseTrackMinColor - trackLayer.thumbWidth = thumbWidth - trackLayer.value = value + trackLayer.trackBackgroundColor = trackConfiguration.maxColor.cgColor + trackLayer.fillColor = direction == .leftToRight ? trackConfiguration.minColor.cgColor : trackConfiguration.reverseMinColor.cgColor trackLayer.cornerRadius = cornerRadius - updateThumbLayersText() - updateThumbLayersPosition() } private func redrawLayers() { + isDirectionChangeAnimationInProgress = true + animateThumbLayer { + self.isDirectionChangeAnimationInProgress = false + } + trackLayer.displayIfNeeded() + + CATransaction.begin() + CATransaction.setAnimationDuration(.zero) + minimumLayer.position = positionForValue(value: minimum) + maximumLayer.position = positionForValue(value: maximum) + CATransaction.commit() + } + + private func animateThumbLayer(_ completionBlock: (() -> Void)? = nil) { + let thumbSize = thumbConfiguration.size + CATransaction.begin() + CATransaction.setCompletionBlock(completionBlock) + CATransaction.setAnimationDuration(animationStyle.animationDuration) + CATransaction.setAnimationTimingFunction(CATransaction.animationTimingFunction()) + thumbLayer.position = positionForValue(value: value) + trackLayer.updateFillLayerForAnimation(CGRect(origin: CGPoint(x: thumbLayer.position.x, y: .zero), + size: CGSize(width: thumbSize.width / 2, height: thumbSize.height))) + updateSlider() + CATransaction.commit() + } + + private func setNeedsLayersDisplay() { trackLayer.setNeedsDisplay() thumbLayer.setNeedsDisplay() - trackMaskLayer.setNeedsDisplay() } private func updateThumbLayersText() { - thumbLayer.trackMinColor = direction == .leftToRight ? trackMinColor : reverseTrackMinColor - thumbLayer.trackMaxColor = trackMaxColor + thumbLayer.foregroundColor = direction == .leftToRight ? trackConfiguration.minColor.cgColor : trackConfiguration.reverseMinColor.cgColor + thumbLayer.backgroundColor = thumbConfiguration.backgroundColor.cgColor thumbLayer.string = textForValue(value) minimumLayer.string = textForValue(minimum) maximumLayer.string = textForValue(maximum) } - private func updateThumbLayersPosition() { + @objc + private func updateSlider() { + let valueRatio = (value - minimum) / (maximum - minimum) + let thumbSize = thumbConfiguration.size + let thumbPositionX = valueRatio * (bounds.width - thumbSize.width) + (value == minimum ? thumbSize.width : thumbSize.width / 2) + let fillFrame: CGRect + switch direction { + case .leftToRight: + fillFrame = CGRect(x: .zero, + y: .zero, + width: thumbPositionX, + height: bounds.height) + case .rightToLeft: + fillFrame = CGRect(x: bounds.width - thumbPositionX, + y: .zero, + width: thumbPositionX, + height: bounds.height) + + } CATransaction.begin() CATransaction.setDisableActions(true) CATransaction.setAnimationDuration(.zero) - - let thumbCenterX = positionForValue(value: value) - thumbLayer.position = CGPoint(x: thumbCenterX, - y: bounds.height / 2) - let maximumXPosition = positionForValue(value: maximum) - maximumLayer.position = CGPoint(x: maximumXPosition, - y: bounds.height / 2) - let minimumXPosition = positionForValue(value: minimum) - minimumLayer.position = CGPoint(x: minimumXPosition, - y: bounds.height / 2) + thumbLayer.position = positionForValue(value: value) + trackLayer.fillFrame = fillFrame CATransaction.commit() + setNeedsLayersDisplay() } - private func updateLayersValue() { - updateThumbLayersText() - trackLayer.value = value - } - - private func positionForValue(value: CGFloat) -> CGFloat { + private func positionForValue(value: CGFloat) -> CGPoint { + let thumbWidth = thumbConfiguration.size.width + let yPosition = bounds.height / 2 if minimum == maximum { - return thumbWidth / 2 + return CGPoint(x: thumbWidth / 2, y: yPosition) + } + let xPosition: CGFloat + switch direction { + case .leftToRight: + xPosition = usableTrackingLength * (value - minimum) / (maximum - minimum) + thumbWidth / 2 + case .rightToLeft: + xPosition = bounds.width - (usableTrackingLength * (value - minimum) / (maximum - minimum) + thumbWidth / 2) } - return usableTrackingLength * (value - minimum) / (maximum - minimum) + thumbWidth / 2 + return CGPoint(x: xPosition, y: yPosition) } - private func trackRectForBound(_ bound: CGRect) -> CGRect { - return CGRect(x: trackInset, - y: (bound.height - trackHeight) / 2, - width: bound.width - 2 * trackInset, - height: trackHeight) + private func trackRectForBounds() -> CGRect { + CGRect(x: trackConfiguration.inset, + y: (bounds.height - trackConfiguration.height) / 2, + width: bounds.width - 2 * trackConfiguration.inset, + height: trackConfiguration.height) } private func textForValue(_ value: CGFloat) -> String { - guard let delegate = delegate else { + guard let delegate else { return String(format: "%.0f", value) } diff --git a/Sources/SliderTrackLayer.swift b/Sources/SliderTrackLayer.swift index f47a510..a7cb9fe 100644 --- a/Sources/SliderTrackLayer.swift +++ b/Sources/SliderTrackLayer.swift @@ -22,88 +22,73 @@ // THE SOFTWARE. // -import UIKit +import Foundation +import QuartzCore +import CoreGraphics -public final class SliderTrackLayer: CALayer { +final class SliderTrackLayer: CALayer { - // MARK: - Properties + // MARK: Properties - public var value: CGFloat = .zero - public var minimumValue: CGFloat = .zero - public var maximumValue: CGFloat = 1 - public var thumbWidth: CGFloat = .zero - public var trackMaxColor: UIColor! - public var trackMinColor: UIColor! - - // MARK: - Initial methods - - public init(value: CGFloat = .zero, - minimumValue: CGFloat = .zero, - maximumValue: CGFloat = 1, - thumbWidth: CGFloat = .zero, - trackMaxColor: UIColor, - trackMinColor: UIColor) { - self.value = value - self.minimumValue = minimumValue - self.maximumValue = maximumValue - self.thumbWidth = thumbWidth - self.trackMaxColor = trackMaxColor - self.trackMinColor = trackMinColor - super.init() + var trackBackgroundColor: CGColor = CGColor(gray: .zero, alpha: 1) { + didSet { + backgroundLayer.backgroundColor = trackBackgroundColor + } + } + var fillColor: CGColor! { + didSet { + fillLayer.backgroundColor = fillColor + } + } + var fillFrame: CGRect = .zero { + didSet { + fillLayer.frame = fillFrame + } } + private let backgroundLayer = CALayer() + private let fillLayer = CALayer() - public override init(layer: Any) { + // MARK: Initial methods + + override init(layer: Any) { super.init(layer: layer) + + configureLayer() } - public override init() { + override init() { super.init() + + configureLayer() } @available(*, unavailable) required init?(coder: NSCoder) { - super.init(coder: coder) + fatalError("init(coder:) has not been implemented") } - // MARK: - Life cycle + // MARK: Life cycle - override public func draw(in ctx: CGContext) { - assert(trackMaxColor != nil, "trackMaxColor should not be nil. Check the color initialization.") - assert(trackMinColor != nil, "trackMinColor should not be nil. Check the color initialization.") - ctx.setFillColor(trackMaxColor.cgColor) - ctx.addPath(UIBezierPath(roundedRect: bounds, cornerRadius: cornerRadius).cgPath) - ctx.fillPath() + override func layoutSublayers() { + super.layoutSublayers() - let trackWidth = bounds.width - cornerRadius - let range = maximumValue - minimumValue - var thresholdX: CGFloat = ((value - minimumValue) / range * trackWidth) - let averangeValue = value - minimumValue - if averangeValue > .zero { - if averangeValue >= maximumValue / 2 { - thresholdX += thumbWidth / 6 - } else { - thresholdX += thumbWidth / 3 - } - } - if value == maximumValue { - thresholdX += -thumbWidth / 3 - } - let width = thresholdX.rounded(.down) - let trackMinSize = CGSize(width: width, height: bounds.height) - let trackMinRect = CGRect(origin: .zero, size: trackMinSize) - let trackMinPath = UIBezierPath(roundedRect: trackMinRect, cornerRadius: cornerRadius) - ctx.setFillColor(trackMinColor.cgColor) - ctx.addPath(trackMinPath.cgPath) - ctx.fillPath() - - let widthDifference = value >= minimumValue ? .zero : bounds.width - thresholdX - let trackMaxRect = CGRect(x: thresholdX - cornerRadius, y: .zero, - width: widthDifference, - height: bounds.height) - let trackMaxPath = UIBezierPath(roundedRect: trackMaxRect, - cornerRadius: cornerRadius) - ctx.setFillColor(trackMinColor.cgColor) - ctx.addPath(trackMaxPath.cgPath) - ctx.fillPath() + backgroundLayer.frame = bounds + backgroundLayer.cornerRadius = cornerRadius + fillLayer.cornerRadius = cornerRadius + } + + // MARK: Public methods + + func updateFillLayerForAnimation(_ frame: CGRect) { + fillFrame = frame + } + + // MARK: Private methods + + private func configureLayer() { + backgroundLayer.masksToBounds = true + backgroundLayer.backgroundColor = trackBackgroundColor + addSublayer(backgroundLayer) + backgroundLayer.addSublayer(fillLayer) } } diff --git a/Sources/TextLayer.swift b/Sources/TextLayer.swift new file mode 100644 index 0000000..576de28 --- /dev/null +++ b/Sources/TextLayer.swift @@ -0,0 +1,41 @@ +// +// TextLayer.swift +// +// Copyright (c) 2024 Ramiz Kichibekov (https://github.com/ramiz69) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import QuartzCore +import CoreGraphics + +final class TextLayer: CATextLayer { + + override func draw(in ctx: CGContext) { + let height = bounds.size.height + let yDiff = (height - fontSize) / 2 - fontSize / 10 + + ctx.saveGState() + ctx.translateBy(x: .zero, y: yDiff) + super.draw(in: ctx) + + ctx.restoreGState() + } + +} diff --git a/Sources/SliderTextLayer.swift b/Sources/ThumbLayer.swift similarity index 51% rename from Sources/SliderTextLayer.swift rename to Sources/ThumbLayer.swift index 00322b9..af8404a 100644 --- a/Sources/SliderTextLayer.swift +++ b/Sources/ThumbLayer.swift @@ -1,7 +1,7 @@ // -// SliderTextLayer.swift +// ThumbLayer.swift // -// Copyright (c) 2020 Ramiz Kichibekov (https://github.com/ramiz69) +// Copyright (c) 2024 Ramiz Kichibekov (https://github.com/ramiz69) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -22,45 +22,21 @@ // THE SOFTWARE. // -import UIKit +import Foundation +import QuartzCore +import CoreGraphics -public class SliderTextLayer: CATextLayer { +final class ThumbLayer: CATextLayer { - // MARK: - Properties + // MARK: Life cycle - public var trackMaxColor: UIColor! - public var trackMinColor: UIColor! - - // MARK: - Initial methods - - public init(trackMaxColor: UIColor, trackMinColor: UIColor) { - self.trackMaxColor = trackMaxColor - self.trackMinColor = trackMinColor - super.init() - } - - public override init(layer: Any) { - super.init(layer: layer) - } - - public override init() { - super.init() - } - - @available(*, unavailable) - required init?(coder: NSCoder) { - super.init(coder: coder) - } - - // MARK: - Life cycle - - public override func setNeedsDisplay() { + override func setNeedsDisplay() { super.setNeedsDisplay() configureBorder() } - override public func draw(in ctx: CGContext) { + override func draw(in ctx: CGContext) { let height = bounds.size.height let yDiff = (height - fontSize) / 2 - fontSize / 10 @@ -71,28 +47,9 @@ public class SliderTextLayer: CATextLayer { ctx.restoreGState() } - open func configureBorder() { - assert(trackMaxColor != nil, "trackMaxColor should not be nil. Check the color initialization.") - assert(trackMinColor != nil, "trackMinColor should not be nil. Check the color initialization.") - foregroundColor = trackMinColor.cgColor + private func configureBorder() { borderWidth = 4 - borderColor = trackMinColor.cgColor - } - -} - -public final class SliderMinimumTextLayer: SliderTextLayer { - - public override func configureBorder() { - - } - -} - -public final class SliderMaximumTextLayer: SliderTextLayer { - - public override func configureBorder() { - + borderColor = foregroundColor } } diff --git a/example.gif b/example.gif deleted file mode 100644 index effbe75..0000000 Binary files a/example.gif and /dev/null differ