diff --git a/IMSwitch/.gitignore b/IMSwitch/.gitignore new file mode 100644 index 0000000..0023a53 --- /dev/null +++ b/IMSwitch/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/IMSwitch/.swiftpm/xcode/xcshareddata/xcschemes/IMSwitch.xcscheme b/IMSwitch/.swiftpm/xcode/xcshareddata/xcschemes/IMSwitch.xcscheme new file mode 100644 index 0000000..4712395 --- /dev/null +++ b/IMSwitch/.swiftpm/xcode/xcshareddata/xcschemes/IMSwitch.xcscheme @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/IMSwitch/Package.swift b/IMSwitch/Package.swift new file mode 100644 index 0000000..3405ed9 --- /dev/null +++ b/IMSwitch/Package.swift @@ -0,0 +1,21 @@ +// swift-tools-version: 5.9 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "IMSwitch", + platforms: [.iOS(.v15), .macOS(.v12)], + products: [ + .library( + name: "IMSwitch", + targets: ["IMSwitch"]), + ], + targets: [ + .target( + name: "IMSwitch"), + .testTarget( + name: "IMSwitchTests", + dependencies: ["IMSwitch"]), + ] +) diff --git a/IMSwitch/Sources/MyLibrary/IMSwitch.swift b/IMSwitch/Sources/MyLibrary/IMSwitch.swift new file mode 100644 index 0000000..298c196 --- /dev/null +++ b/IMSwitch/Sources/MyLibrary/IMSwitch.swift @@ -0,0 +1,224 @@ +import SwiftUI +import Foundation + +class ToggleState: ObservableObject { + @Published var isPrimaryOptionSelected: Bool = false + @Published var isRTL: Bool = false +} + +struct IMSwitch: View { + @StateObject var toggleState: ToggleState + @Environment(\.layoutDirection) var direction + + private (set) var leftImage: Image + private (set) var rightImage: Image + private (set) var backgroundColor: Color + private (set) var circleFilledColor: Color + private (set) var animationType: Animation + private (set) var animationLength: Double + private (set) var activeColor: Color + + init(leftImage: Image, + rightImage: Image, + activeColor: Color, + circleFilledColor: Color, + backgroundColor: Color, + animation: Animation = .default, + animationLength: Double = 0.35, + toggleState: ToggleState) { + self.circleFilledColor = circleFilledColor + self.leftImage = leftImage + self.rightImage = rightImage + self.activeColor = activeColor + self.backgroundColor = backgroundColor + self.animationType = animation + self.animationLength = animationLength + self._toggleState = .init(wrappedValue: toggleState) + } + + var body: some View { + GeometryReader { geometry in + HStack { + if !toggleState.isPrimaryOptionSelected { + Spacer() + } + + Circle() + .fill(circleFilledColor) + .frame(width: geometry.size.width / 2, + height: geometry.size.height) + .simultaneousGesture( + DragGesture() + .onEnded { value in + if value.translation.width < 0 { + self.toggleState.isPrimaryOptionSelected = true + } else { + self.toggleState.isPrimaryOptionSelected = false + } + } + ) + .onTapGesture { + self.toggleState.isPrimaryOptionSelected.toggle() + } + + if toggleState.isPrimaryOptionSelected { + Spacer() + } + } + + .overlay(content: { + HStack { + leftImage + .resizable() + .scaledToFit() + .frame(width: geometry.size.width / 4, height: geometry.size.height / 2) + .foregroundColor(toggleState.isPrimaryOptionSelected ? activeColor : circleFilledColor) + .onTapGesture { + if !toggleState.isPrimaryOptionSelected { + self.toggleState.isPrimaryOptionSelected.toggle() + } + } + .simultaneousGesture( + DragGesture() + .onEnded { value in + if value.translation.width < 0 { + self.toggleState.isPrimaryOptionSelected = !toggleState.isRTL + } else { + self.toggleState.isPrimaryOptionSelected = toggleState.isRTL + } + } + ) + Spacer() + rightImage + .resizable() + .scaledToFit() + .frame(width: geometry.size.width / 4, height: geometry.size.height / 2) + .foregroundColor(toggleState.isPrimaryOptionSelected ? circleFilledColor : activeColor) + .onTapGesture { + if toggleState.isPrimaryOptionSelected { + self.toggleState.isPrimaryOptionSelected.toggle() + } + } + .simultaneousGesture( + DragGesture() + .onEnded { value in + if value.translation.width < 0 { + self.toggleState.isPrimaryOptionSelected = true + } else { + self.toggleState.isPrimaryOptionSelected = false + } + } + ) + } + .padding(EdgeInsets(top: geometry.size.height / 4, + leading: geometry.size.width / 8, + bottom: geometry.size.height / 4, + trailing: geometry.size.width / 8)) + }) + .frame(width: geometry.size.width, height: geometry.size.height) + .padding(EdgeInsets(top: 20, leading: 10, bottom: 20, trailing: 10)) + .background(backgroundColor) + .animation(animationType, value: animationLength) + .cornerRadius(geometry.size.height / 1.6) + } + .shadow(radius: 8) + .onAppear { + toggleState.isRTL = direction == .rightToLeft + } + } +} + +#if DEBUG +struct IMSwitch_Previews: PreviewProvider { + static var previews: some View { + Group { + Group { + VStack(spacing: 50) { + IMSwitch( + leftImage: .init(systemName: "bicycle"), + rightImage: .init(systemName: "parkingsign.circle"), + activeColor: .white, + circleFilledColor: .yellow, + backgroundColor: Color.white.opacity(0.85), animation: .easeInOut, + animationLength: 0.35, toggleState: .init()) + .frame(width: 200, height: 60) + .environment(\.layoutDirection, .leftToRight) + + IMSwitch( + leftImage: .init(systemName: "car"), + rightImage: .init(systemName: "parkingsign.circle"), + activeColor: .white, + circleFilledColor: .blue, + backgroundColor: Color.white.opacity(0.85), animation: .easeInOut, + animationLength: 0.35, toggleState: .init()) + .frame(width: 200, height: 60) + .environment(\.layoutDirection, .leftToRight) + + IMSwitch( + leftImage: .init(systemName: "bus"), + rightImage: .init(systemName: "train.side.front.car"), + activeColor: .white, + circleFilledColor: .green, + backgroundColor: Color.white.opacity(0.85), animation: .easeInOut, + animationLength: 0.35, toggleState: .init()) + .frame(width: 200, height: 60) + .environment(\.layoutDirection, .leftToRight) + + IMSwitch( + leftImage: .init(systemName: "person.3"), + rightImage: .init(systemName: "person"), + activeColor: .white, + circleFilledColor: .red, + backgroundColor: Color.white.opacity(0.85), animation: .easeInOut, + animationLength: 0.35, toggleState: .init()) + .frame(width: 200, height: 60) + .environment(\.layoutDirection, .leftToRight) + } + }.previewDisplayName("Default preview 1") + } + Group { + VStack(spacing: 50) { + IMSwitch( + leftImage: .init(systemName: "bicycle"), + rightImage: .init(systemName: "parkingsign.circle"), + activeColor: .white, + circleFilledColor: .yellow, + backgroundColor: Color.white.opacity(0.85), animation: .easeInOut, + animationLength: 0.35, toggleState: .init()) + .frame(width: 200, height: 60) + .environment(\.layoutDirection, .leftToRight) + + IMSwitch( + leftImage: .init(systemName: "car"), + rightImage: .init(systemName: "parkingsign.circle"), + activeColor: .white, + circleFilledColor: .blue, + backgroundColor: Color.white.opacity(0.85), animation: .easeInOut, + animationLength: 0.35, toggleState: .init()) + .frame(width: 200, height: 60) + .environment(\.layoutDirection, .leftToRight) + + IMSwitch( + leftImage: .init(systemName: "bus"), + rightImage: .init(systemName: "train.side.front.car"), + activeColor: .white, + circleFilledColor: .green, + backgroundColor: Color.white.opacity(0.85), animation: .easeInOut, + animationLength: 0.35, toggleState: .init()) + .frame(width: 200, height: 60) + .environment(\.layoutDirection, .leftToRight) + + IMSwitch( + leftImage: .init(systemName: "person.3"), + rightImage: .init(systemName: "person"), + activeColor: .white, + circleFilledColor: .red, + backgroundColor: Color.white.opacity(0.85), animation: .easeInOut, + animationLength: 0.35, toggleState: .init()) + .frame(width: 200, height: 60) + .environment(\.layoutDirection, .leftToRight) + } + }.previewDisplayName("Default preview 1") + } +} +#endif diff --git a/IMSwitch/Tests/IMSwitchTests/IMSwitchTests.swift b/IMSwitch/Tests/IMSwitchTests/IMSwitchTests.swift new file mode 100644 index 0000000..8deb6b8 --- /dev/null +++ b/IMSwitch/Tests/IMSwitchTests/IMSwitchTests.swift @@ -0,0 +1,4 @@ +import XCTest +@testable import IMSwitch + +final class MyLibraryTests: XCTestCase {} diff --git a/README.md b/README.md index 6abe563..0dfe8de 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,100 @@ -# IMSwitch + +# IMSwitch + +[![Swift Version](https://img.shields.io/badge/Swift-5.0-orange.svg)](https://swift.org) +[![SPM Compatible](https://img.shields.io/badge/SPM-compatible-brightgreen.svg)](https://swift.org/package-manager/) +[![License](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) + + +![lMSwitch glance](https://github.com/mohamed-arradi/IMSwitch/blob/version/v1.0.0/imgs/switch-hstacks.png) + +IMSwitch is a SwiftUI toggle component with image options. It provides a visually appealing and customizable way to toggle between two options. + An easy to use, customizable UISwitch which contains images. Build entirely in SwiftUI + +![Demo IMSwitch](https://github.com/mohamed-arradi/IMSwitch/blob/version/v1.0.0/imgs/iphone-demo-switch.png) +## Features + +- Smooth animations +- Customizable appearance +- Support for Right-to-Left (RTL) layout + +## Requirements + +- Swift 5.5+ +- iOS 15.0+ + +## Installation + +### Swift Package Manager (SPM) + +Add the following to your `Package.swift` file: + +```swift +.package(url: "https://github.com/mohamed-arradi/IMSwitch.git", from: "1.0.0") +``` + +## Usage + +```swift +// Create an instance of ToggleState +let toggleState = ToggleState() + +// Initialize IMSwitch with images and colors +let imSwitch = IMSwitch( + leftImage: Image(systemName: "bicycle"), + rightImage: Image(systemName: "parkingsign.circle"), + activeColor: .white, + circleFilledColor: .blue, + backgroundColor: Color.white.opacity(0.85), + animation: .easeInOut, + animationLength: 0.35, + toggleState: toggleState +) +``` + +### Properties + +- `leftImage`: The image displayed when the toggle is in the left (inactive) state. +- `rightImage`: The image displayed when the toggle is in the right (active) state. +- `activeColor`: The color applied to the state image. +- `circleFilledColor`: The color of the filling circle that moves along the toggle. +- `backgroundColor`: The background color of the toggle. +- `animation`: The type of animation used for state transitions. +- `animationLength`: The duration of the animation in seconds. +- `isPrimaryOptionSelected`: Represents the state of the primary option. +- `isRTL`: Represents the Right-to-Left (RTL) layout direction. + +### Observing Changes +```swift +import SwiftUI + +struct YourView: View { + @StateObject var toggleState = ToggleState() + + var body: some View { + IMSwitch( + // ... other parameters ... + toggleState: toggleState + ) + .onReceive(toggleState.$isPrimaryOptionSelected) { newValue in + // React to changes in isPrimaryOptionSelected + print("isPrimaryOptionSelected changed to \(newValue)") + } + } +} +``` + + +### Example with RTL + +```swift +IMSwitch( + // ... other parameters ... + toggleState: toggleState +) +.environment(\.layoutDirection, .rightToLeft) +.frame(width: 300, height: 90) +``` +IMSwitch is available under the MIT license. +See the [LICENSE](https://github.com/mohamed-arradi/IMSwitch/blob/main/LICENSE) file for more info. diff --git a/imgs/iphone-demo-switch.png b/imgs/iphone-demo-switch.png new file mode 100644 index 0000000..83a95e4 Binary files /dev/null and b/imgs/iphone-demo-switch.png differ diff --git a/imgs/switch-hstacks.png b/imgs/switch-hstacks.png new file mode 100644 index 0000000..902cfda Binary files /dev/null and b/imgs/switch-hstacks.png differ