├── .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 |
--------------------------------------------------------------------------------