27. Jun 2022
iOSAko vytvoriť Slide to Unlock button vo SwiftUI
Návod nevyžaduje pridanie žiadneho add-onu. Tlačidlo obsahuje natívne SF symboly a zobrazenia. Prezentované komponenty a modifikátory sú dostupné pre iOS 13. Existuje jedna výnimka — vylepšenie uvedené v 6. kroku, ktoré vyžaduje iOS 15. Použité farby nájdete na konci článku.
Krok 1. Pridanie drag gesta
Efekt potiahnutia môžeme dosiahnuť pridaním rozpoznávania gesta ťahania. Modifier .gesture(DragGesture() pripojí k zobrazeniu gesto ťahania. Ak chcete získať prístup k hodnotám gesta a vykonať akcie, použite metódu inštancie .onChanged(:). Jej closure parameter zahŕňa CGSize od počiatočného bodu gesta ťahania do aktuálna pozície - value.translation, kde šírka predstavuje horizontálnu os. Animácie nižšie znázorňujú zmenu veľkosti zobrazení v závislosti od vzdialenosti ťahania.
DraggingComponent obsahujúci gesture modifier je prvým krokom k vytvoreniu zobrazenia UnlockButton.
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)
}
}
Krok 2. Použitie limitov a vizuálnych detailov
Okrem minimálnej šírky musíme obmedziť maximálnu veľkosť komponentu DraggingComponent, ktorý sa dá definovať z parent kontajnera. GeometryReader zmení veľkosť zobrazenia na maximálnu dostupnú hodnotu, toto správanie nie je potrebné pre child zobrazenia.
struct UnlockButton: View {
var body: some View {
GeometryReader { geometry in
ZStack(alignment: .leading) {
DraggingComponent(maxWidth: geometry.size.width)
}
}
.frame(height: 50)
.padding()
}
}
DragGesture má metódu inštancie .onEnded(_:), ktorá pridáva akciu spustenú po skončení gesta. Navyše umožňuje pridať napr9klad haptic feedback a obrázok, ktorý sa vrství pred aktuálne view so zarovnaním na trailing.
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)
}
}
Krok 3. Staging background
Tlačidlo UnlockButton nie je dokončen0 bez pozadia a hintu. BackgroundComponent musí byť umiestnený za vrstvou 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()
}
}
Výborne! Tlačidlo odomknutia je pripravené na použitie 🚀
Golden Touches ✨
Pozor! Nasledujúce kroky môžu spôsobiť výbuch tvorivosti.
Krok 4. Úprava intenzity farieb
SwiftUI má celkom dobré modifiers na prácu s farbami a prispôsobenie zobrazení požadovanému dizajnu. Modifier hueRotation() umožňuje upraviť a animovať dominantné farby. V teórii farieb môže byť odtieň prezentovaný ako kruh a uhly sa používajú ako indikátory zmien farieb.
Pridajme tento modifikátor do 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 potrebuje tiež malú úpravu. V závislosti od hodnoty drag gesta môžeme zmeniť priehľadnosť pre background view, takže pozadie bude postupne menej priehľadné, keď sa približuje k polohe „odomknuté".
struct DraggingComponent: View {
...
RoundedRectangle(cornerRadius: 16)
.fill(Color.blueDark)
.opacity(width / maxWidth)
.frame(width: width)
...
}
Krok 5. Podpora simultánnych gest
Aktuálny vzhľad zobrazenia UnlockButton nemá klasické atribúty tlačidiel – rozpoznávanie gest poklepaním a vizuálnu odozvu na stlačený stav. Najlepším a natívnym spôsobom prístupu k vlastnostiam tlačidiel a ich úprav je použitie ButtonStyleConfiguration. Aby sme dokončili tento krok, vytvoríme základný štýl tlačidla, zabalíme DragComponent do zobrazenia Button a aplikujeme naň vytvorený štýl pomocou modifikátora .buttonStyle() a v neposlednom rade podporíme obe gestá.
A. Vytvorenie štýlu tlačidla
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. Zabalenie komponentu do tlačidla a použitie štýlu tlačidla
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. Podpora simultánnych gest
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
)
...
}
}
Krok 6. Asynchrónnosť
V prípade, že akcia odomknutia vyžaduje odpoveď z backendu, pridajte stav načítania. Pre zjednodušenie nasimulujem request priamo vo view. Vo vašich projektoch sa radšej držte architektonickým patternov, napr. MVVM.
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
}
}
}
Posledné, čo zostáva, je pridať ďalší stav zobrazenia tlačidla v 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)
}
}
Posledné, čo zostáva, je pridať ďalší stav zobrazenia tlačidla v DraggingComponent.
Dokončili ste tutoriál. Dobrá práca! 🥳
Napriek tomu, že SwiftUI je nový framework a má ešte svoje obmedzenia, umožňuje vám vytvárať krásne dizajnové komponenty pomerne rýchlo mnohými rôznymi spôsobmi.
Zdroje
💡 Zdrojový kód nájdete na GitHub-e.
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)
}