├── .swiftformat ├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Package.resolved ├── LICENSE ├── Package.swift ├── Sources └── SlideButton │ ├── ShimmerEffect.swift │ ├── SlideButtonStyling.swift │ └── SlideButton.swift └── README.md /.swiftformat: -------------------------------------------------------------------------------- 1 | --disable andOperator,redundantNilInit,redundantParens,redundantSelf,trailingClosures,unusedArguments,wrapMultilineStatementBraces 2 | --enable blankLineAfterImports,blankLinesBetweenImports 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "swiftformat", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/nicklockwood/SwiftFormat", 7 | "state" : { 8 | "revision" : "4c8386a35e6a287387da041794b71b85d0858760", 9 | "version" : "0.52.8" 10 | } 11 | } 12 | ], 13 | "version" : 2 14 | } 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 NO-COMMENT 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.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: "SlideButton", 8 | platforms: [.iOS(.v15), .macOS(.v12)], 9 | products: [ 10 | // Products define the executables and libraries a package produces, and make them visible to other packages. 11 | .library( 12 | name: "SlideButton", 13 | targets: ["SlideButton"] 14 | ), 15 | ], 16 | dependencies: [ 17 | // Dependencies declare other packages that this package depends on. 18 | // .package(url: /* package url */, from: "1.0.0"), 19 | .package(url: "https://github.com/nicklockwood/SwiftFormat", from: "0.50.4"), 20 | ], 21 | targets: [ 22 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 23 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 24 | .target( 25 | name: "SlideButton", 26 | dependencies: [] 27 | ), 28 | ] 29 | ) 30 | -------------------------------------------------------------------------------- /Sources/SlideButton/ShimmerEffect.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShimmerEffect.swift 3 | // SlideButton by NO-COMMENT 4 | // 5 | 6 | import SwiftUI 7 | 8 | // Credit to: https://github.com/markiv/SwiftUI-Shimmer 9 | struct ShimmerEffect: ViewModifier { 10 | @State private var phase: CGFloat = 1 11 | 12 | let animation = Animation.linear(duration: 4).repeatForever(autoreverses: false) 13 | 14 | func body(content: Content) -> some View { 15 | content 16 | .modifier(AnimatedMask(phase: phase).animation(animation)) 17 | .onAppear { 18 | phase = -0.2 19 | } 20 | } 21 | } 22 | 23 | struct AnimatedMask: AnimatableModifier { 24 | var phase: CGFloat = 0 25 | 26 | var animatableData: CGFloat { 27 | get { phase } 28 | set { phase = newValue } 29 | } 30 | 31 | func body(content: Content) -> some View { 32 | content 33 | .mask(mask.scaleEffect(3)) 34 | } 35 | 36 | private var mask: some View { 37 | LinearGradient( 38 | gradient: Gradient(stops: [ 39 | .init(color: Color.black, location: phase), 40 | .init(color: Color.black.opacity(0.3), location: phase + 0.1), 41 | .init(color: Color.black, location: phase + 0.2), 42 | ]), 43 | startPoint: .bottomTrailing, 44 | endPoint: .topLeading 45 | ) 46 | } 47 | } 48 | 49 | extension View { 50 | @ViewBuilder func shimmerEffect(_ active: Bool = true) -> some View { 51 | if active { 52 | modifier(ShimmerEffect()) 53 | } else { 54 | self 55 | } 56 | } 57 | } 58 | 59 | struct ShimmerEffect_Previews: PreviewProvider { 60 | static var previews: some View { 61 | Text("Hello, World!") 62 | .modifier(ShimmerEffect()) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SlideButton 2 | 3 | SlideButton is a SwiftUI package that provides a customizable slide button that can be swiped to unlock or perform an action. The button consists of a background color, a title, an icon, and an indicator that can be dragged horizontally to unlock or perform an action. The view provides several customizable styling options, such as the size and color of the indicator and background, the text alignment, and whether the text fades or hides behind the indicator. 4 | 5 | https://user-images.githubusercontent.com/20423069/232328779-f6ae204b-7ef6-4e96-93b0-8b7aa57e9617.mov 6 | 7 | 8 | ## Installation 9 | 10 | You can install SlideButton using Swift Package Manager. To add SlideButton to your Xcode project, go to `File` > `Swift Packages` > `Add Package Dependency` and enter the URL `https://github.com/no-comment/SlideButton`. 11 | 12 | ## Usage 13 | 14 | To use SlideButton, import the module `SlideButton` in your SwiftUI view. 15 | 16 | ```swift 17 | import SwiftUI 18 | import SlideButton 19 | ``` 20 | 21 | Create a `SlideButton` by providing a title and a callback that will execute when the user successfully swipes the indicator. 22 | 23 | ```swift 24 | SlideButton("Slide to Unlock") { 25 | await unlockDevice() 26 | } 27 | .padding() 28 | ``` 29 | 30 | You can customize the appearance of the slide button by providing a `Styling` instance. For example, you can change the size and color of the indicator, the alignment of the title text, and whether the text fades or hides behind the indicator. 31 | 32 | ```swift 33 | let styling = SlideButton.Styling( 34 | indicatorSize: 60, 35 | indicatorSpacing: 5, 36 | indicatorColor: .accentColor, 37 | backgroundColor: .accentColor.opacity(0.3), 38 | textColor: .secondary, 39 | indicatorSystemName: "chevron.right", 40 | indicatorDisabledSystemName: "xmark", 41 | textAlignment: .center, 42 | textFadesOpacity: true, 43 | textHiddenBehindIndicator: true, 44 | textShimmers: false 45 | ) 46 | 47 | SlideButton("Slide to Unlock", styling: styling) { 48 | await unlockDevice() 49 | } 50 | .padding() 51 | ``` 52 | 53 | ## Documentation 54 | 55 | SlideButton comes with documentation comments to help you understand how to use the package. You can access the documentation by option-clicking on any `SlideButton` or `Styling` instance in your code. 56 | -------------------------------------------------------------------------------- /Sources/SlideButton/SlideButtonStyling.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SlideButtonStyling.swift 3 | // SlideButton by NO-COMMENT 4 | // 5 | 6 | import SwiftUI 7 | 8 | /// A struct that defines the styling options for a `SlideButton`. 9 | public struct SlideButtonStyling { 10 | /// Initializes a new `Styling` instance with the given options. 11 | /// - Parameters: 12 | /// - indicatorSize: The size of the indicator. Default is `60`. 13 | /// - indicatorSpacing: The spacing between the indicator and the border / button title. Default is `5`. 14 | /// - indicatorColor: The color of the indicator. Default is `.accentColor`. 15 | /// - indicatorShape: The shape type of the indicator. Default is `.circular`. 16 | /// - indicatorRotatesForRTL: Whether to rotate the indicator for right-to-left layout or not. Default is `true`. 17 | /// - indicatorBrightness: The brightness of the indicator if enabled. Default is `0.0`. 18 | /// - backgroundColor: The color of the background. Default is `nil`, which sets the background color to a 30% opacity version of the indicator color. 19 | /// - textColor: The color of the title text. Default is `.secondary`. 20 | /// - indicatorSystemName: The system name of the icon used for the indicator. Default is `"chevron.right"`. 21 | /// - indicatorDisabledSystemName: The system name of the icon used for the disabled indicator. Default is `"xmark"`. 22 | /// - textAlignment: The alignment of the title text. Default is `.center`. 23 | /// - textFadesOpacity: A Boolean value that determines whether the title text fades as the indicator is dragged. Default is `true`. 24 | /// - textHiddenBehindIndicator: A Boolean value that determines whether the part of the title text that the indicator passes disappears. Default is `true`. 25 | /// - textShimmers: A Boolean value that determines whether the text should have a shimmering effect. Default is `true`. 26 | public init( 27 | indicatorSize: CGFloat = 60, 28 | indicatorSpacing: CGFloat = 5, 29 | indicatorColor: Color = .accentColor, 30 | indicatorShape: ShapeType = .circular, 31 | indicatorRotatesForRTL: Bool = true, 32 | indicatorBrightness: Double = 0.0, 33 | backgroundColor: Color? = nil, 34 | textColor: Color = .secondary, 35 | indicatorSystemName: String = "chevron.right", 36 | indicatorDisabledSystemName: String = "xmark", 37 | textAlignment: SlideTextAlignment = .center, 38 | textFadesOpacity: Bool = true, 39 | textHiddenBehindIndicator: Bool = true, 40 | textShimmers: Bool = false 41 | ) { 42 | self.indicatorSize = indicatorSize 43 | self.indicatorSpacing = indicatorSpacing 44 | self.indicatorShape = indicatorShape 45 | self.indicatorBrightness = indicatorBrightness 46 | self.indicatorRotatesForRTL = indicatorRotatesForRTL 47 | 48 | self.indicatorColor = indicatorColor 49 | self.backgroundColor = backgroundColor ?? indicatorColor.opacity(0.3) 50 | self.textColor = textColor 51 | 52 | self.indicatorSystemName = indicatorSystemName 53 | self.indicatorDisabledSystemName = indicatorDisabledSystemName 54 | self.textAlignment = textAlignment 55 | self.textFadesOpacity = textFadesOpacity 56 | self.textHiddenBehindIndicator = textHiddenBehindIndicator 57 | self.textShimmers = textShimmers 58 | } 59 | 60 | var indicatorSize: CGFloat 61 | var indicatorSpacing: CGFloat 62 | var indicatorShape: ShapeType 63 | var indicatorRotatesForRTL: Bool 64 | var indicatorBrightness: Double 65 | 66 | var indicatorColor: Color 67 | var backgroundColor: Color 68 | var textColor: Color 69 | 70 | var indicatorSystemName: String 71 | var indicatorDisabledSystemName: String 72 | 73 | var textAlignment: SlideTextAlignment 74 | var textFadesOpacity: Bool 75 | var textHiddenBehindIndicator: Bool 76 | var textShimmers: Bool 77 | 78 | public static let `default`: Self = .init() 79 | } 80 | 81 | public enum ShapeType { 82 | case circular, rectangular(cornerRadius: Double? = 0) 83 | } 84 | 85 | /// An enumeration that defines the alignment options for the title text in a `SlideButton`. 86 | public enum SlideTextAlignment { 87 | /// The title text is aligned to the leading edge of the title space. 88 | case leading 89 | /// The title text is aligned to the center of the button, not shifted for the indicator. 90 | case globalCenter 91 | /// The title text is aligned to the horizontal center of the title space. 92 | case center 93 | /// The title text is aligned to the trailing edge of the title space. 94 | case trailing 95 | 96 | var horizontalAlignment: HorizontalAlignment { 97 | switch self { 98 | case .leading: 99 | return .leading 100 | case .center, .globalCenter: 101 | return .center 102 | case .trailing: 103 | return .trailing 104 | } 105 | } 106 | 107 | var textAlignment: TextAlignment { 108 | switch self { 109 | case .leading: 110 | return .leading 111 | case .center, .globalCenter: 112 | return .center 113 | case .trailing: 114 | return .trailing 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /Sources/SlideButton/SlideButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SlideButton.swift 3 | // SlideButton by NO-COMMENT 4 | // 5 | 6 | import SwiftUI 7 | 8 | /// A view that presents a slide button that can be swiped to unlock or perform an action. 9 | public struct SlideButton: View { 10 | @Environment(\.isEnabled) private var isEnabled 11 | 12 | private let title: Label 13 | private let callback: () async -> Void 14 | 15 | public typealias Styling = SlideButtonStyling 16 | private let styling: Styling 17 | 18 | @GestureState private var offset: CGFloat 19 | @State private var swipeState: SwipeState = .start 20 | 21 | @Environment(\.layoutDirection) private var layoutDirection 22 | 23 | // When layoutdirection is RTL, the indicatorshape will be right aligned 24 | // instead of left aligned and values need to be negated 25 | private var layoutDirectionMultiplier: Double { 26 | self.layoutDirection == .rightToLeft ? -1 : 1 27 | } 28 | 29 | /// Initializes a slide button with the given title, styling options, and action. 30 | /// 31 | /// Use this initializer to create a new instance of `SlideButton` with the given title, styling, and callback. The `styling` parameter allows you to customize the appearance of the slide button, such as changing the size and color of the indicator, the alignment of the title text, and whether the text fades or hides behind the indicator. The `action` parameter is executed when the user successfully swipes the indicator. 32 | /// 33 | /// - Parameters: 34 | /// - styling: The styling options to customize the appearance of the slide button. Default is `.default`. 35 | /// - action: The async callback action that is executed when the user successfully swipes the indicator. 36 | /// - label: The function creating a label view 37 | public init(styling: Styling = .default, action: @escaping () async -> Void, @ViewBuilder label: () -> Label) { 38 | self.title = label() 39 | self.callback = action 40 | self.styling = styling 41 | 42 | self._offset = .init(initialValue: styling.indicatorSpacing) 43 | } 44 | 45 | @ViewBuilder 46 | private var indicatorShape: some View { 47 | switch styling.indicatorShape { 48 | case .circular: 49 | Circle() 50 | case let .rectangular(cornerRadius): 51 | RoundedRectangle(cornerRadius: max(0, (cornerRadius ?? 0) - styling.indicatorSpacing)) 52 | } 53 | } 54 | 55 | @ViewBuilder 56 | private var mask: some View { 57 | switch styling.indicatorShape { 58 | case .circular: 59 | Capsule() 60 | case let .rectangular(cornerRadius): 61 | RoundedRectangle(cornerRadius: cornerRadius ?? 0) 62 | } 63 | } 64 | 65 | public var body: some View { 66 | GeometryReader { reading in 67 | let calculatedOffset: CGFloat = swipeState == .swiping ? offset : (swipeState == .start ? styling.indicatorSpacing : (reading.size.width - styling.indicatorSize + styling.indicatorSpacing)) 68 | ZStack(alignment: .leading) { 69 | styling.backgroundColor 70 | .saturation(isEnabled ? 1 : 0) 71 | 72 | ZStack { 73 | if styling.textAlignment == .globalCenter { 74 | title 75 | .multilineTextAlignment(styling.textAlignment.textAlignment) 76 | .foregroundColor(styling.textColor) 77 | .frame(maxWidth: max(0, reading.size.width - 2 * styling.indicatorSpacing), alignment: .center) 78 | .padding(.horizontal, styling.indicatorSize) 79 | .shimmerEffect(isEnabled && styling.textShimmers) 80 | } else { 81 | title 82 | .multilineTextAlignment(styling.textAlignment.textAlignment) 83 | .foregroundColor(styling.textColor) 84 | .frame(maxWidth: max(0, reading.size.width - 2 * styling.indicatorSpacing), alignment: Alignment(horizontal: styling.textAlignment.horizontalAlignment, vertical: .center)) 85 | .padding(.trailing, styling.indicatorSpacing) 86 | .padding(.leading, styling.indicatorSize) 87 | .shimmerEffect(isEnabled && styling.textShimmers) 88 | } 89 | } 90 | .opacity(styling.textFadesOpacity ? (1 - progress(from: styling.indicatorSpacing, to: reading.size.width - styling.indicatorSize + styling.indicatorSpacing, current: calculatedOffset)) : 1) 91 | .animation(.interactiveSpring(), value: calculatedOffset) 92 | .mask { 93 | if styling.textHiddenBehindIndicator { 94 | Rectangle() 95 | .overlay(alignment: .leading) { 96 | Color.red 97 | .frame(width: calculatedOffset + (0.5 * styling.indicatorSize - styling.indicatorSpacing)) 98 | .frame(maxWidth: .infinity, alignment: .leading) 99 | .animation(.interactiveSpring(), value: swipeState) 100 | .blendMode(.destinationOut) 101 | } 102 | } else { 103 | Rectangle() 104 | } 105 | } 106 | .frame(maxWidth: .infinity, alignment: .trailing) 107 | 108 | indicatorShape 109 | .brightness(isEnabled ? styling.indicatorBrightness : 0) 110 | .frame(width: styling.indicatorSize - 2 * styling.indicatorSpacing, height: styling.indicatorSize - 2 * styling.indicatorSpacing) 111 | .foregroundColor(isEnabled ? styling.indicatorColor : .gray) 112 | .overlay(content: { 113 | ZStack { 114 | ProgressView().progressViewStyle(.circular) 115 | .tint(.white) 116 | .opacity(swipeState == .end ? 1 : 0) 117 | Image(systemName: isEnabled ? styling.indicatorSystemName : styling.indicatorDisabledSystemName) 118 | .foregroundColor(.white) 119 | .font(.system(size: max(0.4 * styling.indicatorSize, 0.5 * styling.indicatorSize - 2 * styling.indicatorSpacing), weight: .semibold)) 120 | .opacity(swipeState == .end ? 0 : 1) 121 | .rotationEffect(Angle(degrees: styling.indicatorRotatesForRTL && self.layoutDirection == .rightToLeft ? 180 : 0)) 122 | } 123 | }) 124 | .offset(x: calculatedOffset) 125 | .animation(.interactiveSpring(), value: swipeState) 126 | .gesture( 127 | DragGesture() 128 | .updating($offset) { value, state, transaction in 129 | guard swipeState != .end else { return } 130 | 131 | if swipeState == .start { 132 | DispatchQueue.main.async { 133 | swipeState = .swiping 134 | #if os(iOS) 135 | UIImpactFeedbackGenerator(style: .light).prepare() 136 | #endif 137 | } 138 | } 139 | 140 | let val = value.translation.width * layoutDirectionMultiplier 141 | 142 | state = clampValue(value: val, min: styling.indicatorSpacing, max: reading.size.width - styling.indicatorSize + styling.indicatorSpacing) 143 | } 144 | .onEnded { value in 145 | guard swipeState == .swiping else { return } 146 | swipeState = .end 147 | 148 | let predictedVal = value.predictedEndTranslation.width * layoutDirectionMultiplier 149 | let val = value.translation.width * layoutDirectionMultiplier 150 | 151 | if predictedVal > reading.size.width 152 | || val > reading.size.width - styling.indicatorSize - 2 * styling.indicatorSpacing { 153 | Task { 154 | #if os(iOS) 155 | UIImpactFeedbackGenerator(style: .light).impactOccurred() 156 | #endif 157 | 158 | await callback() 159 | swipeState = .start 160 | } 161 | 162 | } else { 163 | swipeState = .start 164 | #if os(iOS) 165 | UIImpactFeedbackGenerator(style: .light).impactOccurred() 166 | #endif 167 | } 168 | } 169 | ) 170 | } 171 | .mask({ mask }) 172 | } 173 | .frame(height: styling.indicatorSize) 174 | .accessibilityRepresentation { 175 | Button(action: { 176 | swipeState = .end 177 | 178 | Task { 179 | await callback() 180 | swipeState = .start 181 | } 182 | }, label: { 183 | title 184 | }) 185 | .disabled(swipeState != .start) 186 | } 187 | } 188 | 189 | private func clampValue(value: CGFloat, min minValue: CGFloat, max maxValue: CGFloat) -> CGFloat { 190 | return max(minValue, min(maxValue, value)) 191 | } 192 | 193 | private func progress(from start: Double, to end: Double, current: Double) -> Double { 194 | let clampedCurrent = max(min(current, end), start) 195 | return (clampedCurrent - start) / (end - start) 196 | } 197 | 198 | private enum SwipeState { 199 | case start, swiping, end 200 | } 201 | } 202 | 203 | public extension SlideButton where Label == Text { 204 | @available(*, deprecated, renamed: "init(_:styling:action:)") 205 | init(_ titleKey: LocalizedStringKey, styling: Styling = .default, callback: @escaping () async -> Void) { 206 | self.init(styling: styling, action: callback, label: { Text(titleKey) }) 207 | } 208 | 209 | init(_ titleKey: LocalizedStringKey, styling: Styling = .default, action: @escaping () async -> Void) { 210 | self.init(styling: styling, action: action, label: { Text(titleKey) }) 211 | } 212 | 213 | @available(*, deprecated, renamed: "init(_:styling:action:)") 214 | init(_ title: S, styling: Styling = .default, callback: @escaping () async -> Void) where S: StringProtocol { 215 | self.init(styling: styling, action: callback, label: { Text(title) }) 216 | } 217 | 218 | init(_ title: S, styling: Styling = .default, action: @escaping () async -> Void) where S: StringProtocol { 219 | self.init(styling: styling, action: action, label: { Text(title) }) 220 | } 221 | } 222 | 223 | #if DEBUG 224 | @available(iOS 16.0, *) 225 | @available(macOS 16.0, *) 226 | struct SlideButton_Previews: PreviewProvider { 227 | struct ContentView: View { 228 | var body: some View { 229 | ScrollView { 230 | VStack(spacing: 25) { 231 | SlideButton("Centered text and lorem ipsum dolor sit", action: sliderCallback) 232 | SlideButton("Leading text and no fade", styling: .init(textAlignment: .leading, textFadesOpacity: false), action: sliderCallback) 233 | SlideButton("Center text and no mask", styling: .init(textHiddenBehindIndicator: false), action: sliderCallback) 234 | SlideButton("Remaining space center", styling: .init(indicatorColor: .red, indicatorSystemName: "trash"), action: sliderCallback) 235 | SlideButton("Trailing and immediate response", styling: .init(textAlignment: .trailing), action: sliderCallback) 236 | SlideButton("Global center", styling: .init(indicatorColor: .red, indicatorSystemName: "trash", textAlignment: .globalCenter, textShimmers: true), action: sliderCallback) 237 | SlideButton("Spacing 15", styling: .init(indicatorSpacing: 15), action: sliderCallback) 238 | SlideButton("Big", styling: .init(indicatorSize: 100), action: sliderCallback) 239 | SlideButton("disabled green", styling: .init(indicatorColor: .green), action: sliderCallback) 240 | .disabled(true) 241 | SlideButton("disabled", action: sliderCallback) 242 | .disabled(true) 243 | }.padding(.horizontal) 244 | } 245 | } 246 | 247 | private func sliderCallback() async { 248 | try? await Task.sleep(for: .seconds(2)) 249 | } 250 | } 251 | 252 | static var previews: some View { 253 | ContentView() 254 | } 255 | } 256 | #endif 257 | --------------------------------------------------------------------------------