27. Jun 2022iOS

Ako 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.

Valeriia AbelovskaiOS Developer

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)

}
Valeriia AbelovskaiOS Developer