Přeskočit na obsah
iOS

Jak vytvořit button Slide to Unlock ve SwiftUI

28. 2. 2023
Jak vytvořit button Slide to Unlock ve SwiftUI

Návod nevyžaduje přidání žádného add-onu. Tlačítko obsahuje nativní symboly SF a zobrazení. Prezentované komponenty a modifikátory jsou k dispozici pro iOS 13. Existuje jedna výjimka – vylepšení uvedené v 6. kroku, které vyžaduje iOS 15. Použité barvy najdete na konci článku.

Krok 1. Přidání drag gesta

Efektu potáhnutí můžeme dosáhnout přidáním rozpoznávání gesta přetažení. Modifikátor .gesture(DragGesture() připojí k zobrazení gesto přetažení. Pokud chcete získat přístup k hodnotám gesta a provádět akce, použijte metodu instance .onChanged(:). Její closure parametr zahrnuje CGSize od počátečního bodu gesta přetažení do aktuální pozice – value.translation, kde šířka představuje horizontální osu. Následující animace znázorňují změnu velikosti zobrazení v závislosti na vzdálenosti přetažení.

 

Prvním krokem k vytvoření zobrazení UnlockButton je DraggingComponent obsahující gesture modifier.

‍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žití limitů a vizuálních detailů

Kromě minimální šířky musíme omezit i maximální velikost komponenty DraggingComponent, kterou lze definovat z parent kontejneru. GeometryReader změní velikost zobrazení na maximální dostupnou hodnotu. Toto chování není pro child zobrazení potřeba.

struct UnlockButton: View { var body: some View { GeometryReader { geometry in ZStack(alignment: .leading) { DraggingComponent(maxWidth: geometry.size.width) } } .frame(height: 50) .padding() } }

DragGesture má metodu instance .onEnded(_:), která přidává akci spuštěnou po dokončení gesta. Kromě toho umožňuje přidat například haptic feedback a obrázek, který se vrství před aktuální view se zarovnání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čítko UnlockButton by nebylo kompletní bez pozadí a hintu. BackgroundComponent musí být umístěna 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ýborně! Tlačítko odemknutí je připraveno k použití 🚀

Golden Touches ✨

Pozor! Následující kroky mohou způsobit výbuch tvořivosti.

Krok 4. Nastavení intenzity barev

SwiftUI má poměrně dobré modifiers pro práci s barvami a přizpůsobení zobrazení požadovanému designu. Modifikátor hueRotation() umožňuje upravovat a animovat dominantní barvy. V teorii barev může být odstín prezentovaný jako kruh a úhly slouží jako indikátory změn barev.

Přidejme tento modifikátor do komponenty 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 je taky třeba trochu upravit. V závislosti na hodnotě drag gesta můžeme změnit průhlednost pro background view tak, aby se pozadí spolu s přibližováním k poloze „odemčeno“ postupně stávalo méně průhledným.

struct DraggingComponent: View { ... RoundedRectangle(cornerRadius: 16) .fill(Color.blueDark) .opacity(width / maxWidth) .frame(width: width) ... }

 

Krok 5. Podpora simultánních gest

Aktuální vzhled zobrazení UnlockButton nemá klasické atributy tlačítek – rozpoznávání gest poklepáním a vizuální odezvu na stav stisknuto. Nejlepším a nativním způsobem, jak přistupovat k vlastnostem tlačítek a jejich úpravám, je použití ButtonStyleConfiguration. Pro dokončení tohoto kroku vytvoříme základní styl tlačítka, zabalíme DragComponent do zobrazení Button a aplikujeme na ni vytvořený styl pomocí modifikátoru .buttonStyle(). V neposlední řadě podpoříme obě gesta.

A. Vytvoření stylu tlačítka

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. Zabalení komponenty do tlačítka a použití stylu tlačítka

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ánních 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. Asynchronicita

Pokud akce odemknutí vyžaduje odpověď z backendu, přidejte stav načítání. Pro zjednodušení budu simulovat request přímo ve view. V projektech se radši držte architektonických patternů, například 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 } } }

A na závěr zbývá přidat další stav zobrazení tlačítka do 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) } }

 

Dokončili jste cvičení. Dobrá práce! 🥳

Ačkoli je SwiftUI nový framework a stále ještě má jistá omezení, umožňuje poměrně rychle vytvářet krásné designové komponenty mnoha různými způsoby.

Zdroje

💡 Zdrojový kód najdete na GitHubu.

‍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) }
Zpět na články
Sdílet dál
Ozvěte se kdykoli

Připraveni rozjet projekt?
Pojďme stavět spolu.

Domluvte si hovor nebo nám napište e-mail.

Kontaktujte nás

Nevíte, kde začít? Stáhněte si naši šablonu produktového zadání.