27. Jun 2022
iOSHow to Make a Slide to Unlock Button in SwiftUI
The tutorial doesn’t require adding any third party. The button is built with native SF Symbols and Views. Presented components & modifiers are available for the iOS 13. There is one exception — an improvement mentioned in the 6th step that requires iOS 15. Used colors can be found at the end of the article.
Step 1. Adding a Drag Gesture
We can achieve a "slide" effect by adding a recognition of the drag gesture. The .gesture(DragGesture() modifier attaches a drag gesture to the view . To access the gesture’s values and perform actions let’s use an instance method .onChanged(:). Its closure parameter includes a CGSize from the starting point of the drag gesture to the current position - value.translation, where width represents a horizontal axis. Animations below illustrate a size change of the views depending on a dragging distance.
The DraggingComponent containing a gesture modifier is the first step to create an UnlockButton view.
struct DraggingComponent: View {
let maxWidth: CGFloat
private let minWidth = CGFloat(50)
@State private var width = CGFloat(50)
var body: some View {
RoundedRectangle(cornerRadius: 16)
.fill(Color.blueDark)
.frame(width: width)
.gesture(
DragGesture()
.onChanged { value in
if value.translation.width > 0 {
width = min(max(value.translation.width + minWidth, minWidth), maxWidth)
}
}
)
.animation(.spring(response: 0.5, dampingFraction: 1, blendDuration: 0), value: width)
}
}
Step 2. Applying Limits & Visual Details
Additionally to minimum width, we need to limit the maximum size of the DraggingComponent, it can be defined from the parent container. GeometryReader changes the view’s size to the maximum available, this behavior is unnecessary for child views.
struct UnlockButton: View {
var body: some View {
GeometryReader { geometry in
ZStack(alignment: .leading) {
DraggingComponent(maxWidth: geometry.size.width)
}
}
.frame(height: 50)
.padding()
}
}
DragGesture has an instance method .onEnded(_:), that adds an action triggred when the gesture ends. Plus let’s add some haptics and an image that layers in front of the current view with trailing alignment.
struct DraggingComponent: View {
@Binding var isLocked: Bool
...
.frame(width: width)
.overlay(
ZStack {
image(name: "lock", isShown: isLocked)
image(name: "lock.open", isShown: !isLocked)
},
alignment: .trailing
)
.gesture(
DragGesture()
.onChanged {
guard isLocked else { return }
...
}
.onEnded { value in
guard isLocked else { return }
if width < maxWidth {
width = minWidth
UINotificationFeedbackGenerator().notificationOccurred(.warning)
} else {
UINotificationFeedbackGenerator().notificationOccurred(.success)
withAnimation(.spring().delay(0.5)) {
isLocked = false
}
}
}
)
...
private func image(name: String, isShown: Bool) -> some View {
Image(systemName: name)
.font(.system(size: 20, weight: .regular, design: .rounded))
.foregroundColor(Color.blueDark)
.frame(width: 42, height: 42)
.background(RoundedRectangle(cornerRadius: 14).fill(.white))
.padding(4)
.opacity(isShown ? 1 : 0)
.scaleEffect(isShown ? 1 : 0.01)
}
}
Step 3. Staging Background
The UnlockButton isn’t completed without a background and a hint note. The BackgroundComponent needs to be placed behind a layer of the DraggingComponent.
struct BackgroundComponent: View {
var body: some View {
ZStack(alignment: .leading) {
RoundedRectangle(cornerRadius: 16)
.fill(Color.blueBright.opacity(0.4))
Text("Slide to unlock")
.font(.footnote)
.bold()
.foregroundColor(.white)
.frame(maxWidth: .infinity)
}
}
}
struct UnlockButton: View {
@State private var isLocked = true
var body: some View {
GeometryReader { geometry in
ZStack(alignment: .leading) {
BackgroundComponent()
DraggingComponent(isLocked: $isLocked, maxWidth: geometry.size.width)
}
}
.frame(height: 50)
.padding()
}
}
Well done! Unlock button is ready to use 🚀
Golden Touches ✨
Caution! Following steps may cause a creativity burst.
Step 4. Adjusting Color Intensity
SwiftUI has pretty convenient modifiers to work with colors and adapt views to the required design. The hueRotation() modifier allows adjust and animate dominant colors. In color theory Hue can be presented as a circle and angles are used as indicators of color changes.
Let’s add this modifier to the BackgroundComponent.
struct BackgroundComponent: View {
@State private var hueRotation = false
var body: some View {
ZStack(alignment: .leading) {
RoundedRectangle(cornerRadius: 16)
.fill(
LinearGradient(
colors: [Color.blueBright.opacity(0.6), Color.blueDark.opacity(0.6)],
startPoint: .leading,
endPoint: .trailing
)
)
.hueRotation(.degrees(hueRotation ? 20 : -20))
...
}
.onAppear {
withAnimation(.linear(duration: 3).repeatForever(autoreverses: true)) {
hueRotation.toggle()
}
}
}
}
DraggingComponent needs a small adjustment too. Depending on the value of the drag gesture we can change transparency of the background view, so the background becomes gradually less transparent as closer it gets to the "unlocked" position.
struct DraggingComponent: View {
...
RoundedRectangle(cornerRadius: 16)
.fill(Color.blueDark)
.opacity(width / maxWidth)
.frame(width: width)
...
}
Step 5. Supporting Simultaneous Gestures
The current appearance of the UnlockButton view doesn’t have classical button attributes — a tap gesture recognizer and a visual response for the pressed state. The best & native way to access and modify button properties is to use the ButtonStyleConfiguration. To complete this step, we will create a base button style, wrap the DraggingComponent into a Button view and apply the created style to it with the .buttonStyle() modifier, and last but not least support both gestures.
A. Creating a Button Style
struct BaseButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.scaleEffect(configuration.isPressed ? 0.95 : 1)
.opacity(configuration.isPressed ? 0.9 : 1)
.animation(.default, value: configuration.isPressed)
}
}
B. Wrapping the Component into the Button & Applying the Button Style
struct DraggingComponent: View {
...
var body: some View {
RoundedRectangle(cornerRadius: 16)
.fill(Color.blueDark)
.opacity(width / maxWidth)
.frame(width: width)
.overlay(
Button(action: { }) {
ZStack {
image(name: "lock", isShown: isLocked)
image(name: "lock.open", isShown: !isLocked)
}
}
.buttonStyle(BaseButtonStyle())
.disabled(!isLocked),
alignment: .trailing
)
...
}
}
C. Supporting Simultaneous Gestures
struct DraggingComponent: View {
...
var body: some View {
RoundedRectangle(cornerRadius: 16)
...
.overlay( ... )
.gesture(
.simultaneousGesture(
DragGesture()
)
...
}
Step 6. Embracing Asynchrony
In case the unlock action requires a response from the backend, let’s add a loading state. To simplify it, I will simulate the request directly in the view, please stick to MVVM in your project.
struct UnlockButton: View {
@State private var isLocked = true
@State private var isLoading = false
var body: some View {
GeometryReader { geometry in
ZStack(alignment: .leading) {
BackgroundComponent()
DraggingComponent(isLocked: $isLocked, isLoading: isLoading, maxWidth: geometry.size.width)
}
}
.frame(height: 50)
.padding()
.onChange(of: isLocked) { isLocked in
guard !isLocked else { return }
simulateRequest()
}
}
private func simulateRequest() {
isLoading = true
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
isLoading = false
}
}
}
The last what left is to add an additional state of the button view in the DraggingComponent.
struct DraggingComponent: View {
@Binding var isLocked: Bool
let isLoading: Bool
let maxWidth: CGFloat
...
.overlay(
Button(action: { }) {
ZStack {
image(name: "lock", isShown: isLocked)
progressView(isShown: isLoading)
image(name: "lock.open", isShown: !isLocked && !isLoading)
}
.animation(.easeIn(duration: 0.35).delay(0.55), value: !isLocked && !isLoading)
}
.buttonStyle(BaseButtonStyle())
.disabled(!isLocked || isLoading),
alignment: .trailing
)
...
private func progressView(isShown: Bool) -> some View {
ProgressView()
.progressViewStyle(.circular)
.tint(.white)
.opacity(isShown ? 1 : 0)
.scaleEffect(isShown ? 1 : 0.01)
}
}
You’ve completed the tutorial. Great job! 🥳
In spite of the fact that the SwiftUI is a young framework with its limitations, it allows you to create beautiful design components pretty fast in so many different ways.
Resources
💡 Source code can be found on GitHub.
extension Color {
static let pinkBright = Color(red: 247/255, green: 37/255, blue: 133/255)
static let blueBright = Color(red: 67/255, green: 97/255, blue: 238/255)
static let blueDark = Color(red: 58/255, green: 12/255, blue: 163/255)
}