Skip to content

Instantly share code, notes, and snippets.

@RubeDEV
Last active May 13, 2020 14:33
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save RubeDEV/c63d1383dedfccd170baa72ed810732d to your computer and use it in GitHub Desktop.
Save RubeDEV/c63d1383dedfccd170baa72ed810732d to your computer and use it in GitHub Desktop.
Tarjeta Deslizable (Slide-Over Card)
import SwiftUI
struct SearchTextFieldStyle: TextFieldStyle {
@Binding var a: String
public func _body(configuration: TextField<Self._Label>) -> some View {
HStack(spacing:0){
Image(systemName: "magnifyingglass")
.foregroundColor(.secondary)
.padding(10)
configuration
if self.a != "" {
Button(action: { self.a = "" }){
Image(systemName: "xmark.circle.fill")
.foregroundColor(Color.secondary).padding(5)
}
}
} .background(Color(UIColor.secondarySystemFill).opacity(0.6)
.cornerRadius(7, antialiased: true))
}
init(a: Binding<String>) {
self._a = a
}
}
struct ContentView: View {
@State var open = TarjetaDesplegada.no
var body: some View {
Text("Hello").tarjetaDeslizable(desplegada: $open, encabezado:TextField("Buscar", text: $a).padding(.bottom).textFieldStyle(SearchTextFieldStyle(a: $a))
.padding(.horizontal, 15)){
Color.gray
}
}
}
/* short description on usage
Attach it to the view you want to hide under the card (intended for use on views that take all or most of the screen).
Init parameters:
-> desplegada: binding that stores current state of card. There are 3 possible states:
* desplegada (open)
* semidesplegada (partially open)
* no (closed)
-> encabezado: View that is always visible (even the card closed). Perfect for search fields, as in Maps App.
-> _ contenido: View shown if card is not closed.
*/
//
// ModeloTarjetaDeslizable.swift
// Settle Abroad
//
// Created by Usuario invitado on 21/03/2020.
// Copyright © 2020 SettleAbroad. All rights reserved.
//
import CoreGraphics
public enum TarjetaDesplegada: CGFloat{
case no = 0.05, semidesplegada = 0.45, desplegada = 0.95
var estaDesplegada: Bool {
self != TarjetaDesplegada.no
}
}
public enum EstadoGestoDeslizamiento {
case inactivo, activo(desplazamiento: CGSize)
var desplazamiento: CGSize{
switch self {
case .activo(let d):
return d
default:
return .zero
}
}
var enDeslizamiento: Bool {
switch self {
case .inactivo:
return false
default:
return true
}
}
}
//
// TarjetaDeslizable.swift
//
// Copyright © 2020 RubeDEV. All rights reserved.
//
import SwiftUI
/** Crea una tarjeta flotante como la de mapas.
- parameter estaAbierta: Comprobamos si la tarjeta está abierta o no.
*/
struct TarjetaDeslizable<Encabezado: View, Contenido: View>: ViewModifier {
///Comprobamos si la tarjeta está abierta o no.
@Binding var deslizada: TarjetaDesplegada
@GestureState var estadoDeslizar = EstadoGestoDeslizamiento.inactivo
private let indicador: some View = Color.secondary.opacity(0.5).frame(width: 40, height: 5).cornerRadius(3, antialiased: true).padding(.vertical, 5)
private let encabezado: Encabezado
private let contenido: Contenido
func body(content: Content) -> some View {
let gesto = DragGesture().updating($estadoDeslizar){ gesto, estado, cambio in
estado = .activo(desplazamiento: gesto.translation)
}
return GeometryReader{ geo in
content
.disabled(self.deslizada == .desplegada)
.overlay((self.deslizada == .desplegada ? Color.black.opacity(0.4) : Color.clear).edgesIgnoringSafeArea(.vertical))
.overlay(
VStack(spacing: 0) {
self.indicador
self.encabezado
if self.deslizada.estaDesplegada {
Divider()
ScrollView{
self.contenido
}.frame(maxHeight: .infinity)
}
}
//.edgesIgnoringSafeArea(.bottom)
.frame(minHeight: geo.size.height * self.deslizada.rawValue)
.background(
Blur(style: .systemMaterial)
.clipShape(FormaTarjeta(radio: 15))
.edgesIgnoringSafeArea(.bottom)
)
.frame(maxWidth: .infinity, idealHeight: geo.size.height*self.deslizada.rawValue, maxHeight: .infinity, alignment:.bottom)
// MARK: Arreglar OFFSET
//.offset(y: self.deslizada.estaDesplegada ? 0 : self.deslizada.rawValue * geo.size.height + self.estadoDeslizar.desplazamiento.height)//self.deslizada.rawValue * geo.size.height)
.animation(self.estadoDeslizar.enDeslizamiento ? nil : .interpolatingSpring(stiffness: 300, damping: 30, initialVelocity: 10))
.shadow(radius: 10)
.animation(.interactiveSpring())
)
.gesture(gesto.onEnded(self.gestoTerminado))
}
}
private func gestoTerminado(drag: DragGesture.Value) {
let direcciónVertical = drag.predictedEndLocation.y - drag.location.y
let posiciónBordeSuperiorTarjeta = self.deslizada.rawValue + drag.translation.height
let posiciónSuperior: TarjetaDesplegada
let posiciónInferior: TarjetaDesplegada
let posiciónMásCercana: TarjetaDesplegada
// ¡¡¡¡Arreglar!!!! HAy que multiplicar el rawValue por la altura de la vista
//MARK: ¡¡¡ARREGLAR!!!
if posiciónBordeSuperiorTarjeta <= TarjetaDesplegada.semidesplegada.rawValue {
posiciónSuperior = .desplegada
posiciónInferior = .semidesplegada
} else {
posiciónSuperior = .semidesplegada
posiciónInferior = .no
}
// Aquí también
if (posiciónBordeSuperiorTarjeta - posiciónSuperior.rawValue) < (posiciónInferior.rawValue - posiciónBordeSuperiorTarjeta) {
posiciónMásCercana = posiciónSuperior
} else {
posiciónMásCercana = posiciónInferior
}
switch direcciónVertical {
case 0:
self.deslizada = posiciónMásCercana
case 0...CGFloat.infinity:
self.deslizada = posiciónInferior
default:
self.deslizada = posiciónSuperior
}
}
init(desplegada: Binding<TarjetaDesplegada>, encabezado: Encabezado, _ contenido: Contenido) {
self._deslizada = desplegada
self.contenido = contenido
self.encabezado = encabezado
}
}
extension View {
func tarjetaDeslizable<Encabezado: View, Contenido:View>(desplegada: Binding<TarjetaDesplegada>,encabezado: Encabezado, _ contenido: () -> Contenido) -> some View {
modifier(TarjetaDeslizable(desplegada: desplegada, encabezado: encabezado, contenido()))
}
}
struct FormaTarjeta: Shape {
let radio: CGFloat
func path(in geo: CGRect) -> Path {
let radio = min(self.radio, geo.size.height, geo.size.width / 2)
return Path{ camino in
camino.move(to: CGPoint(x: radio, y: 0))
camino.addQuadCurve(to: CGPoint(x: 0, y: radio), control: .zero)
camino.addLine(to: CGPoint(x: 0, y: geo.size.height))
camino.addLine(to: CGPoint(x: geo.size.width, y: geo.size.height))
camino.addLine(to: CGPoint(x: geo.size.width, y: radio))
camino.addQuadCurve(to: CGPoint(x: geo.size.width - radio, y: 0), control: CGPoint(x: geo.size.width, y: 0))
}
}
init(radio: CGFloat = 0){
self.radio = radio
}
}
struct VistaTarjetaFlotante_Previews: PreviewProvider {
static var previews: some View {
Group{
Vista()
}
}
struct Vista: View {
@State var desplegada = TarjetaDesplegada.no
@State var a = ""
var body: some View {
Button(action: {}){Text("Hola")}.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.red)
.tarjetaDeslizable(desplegada: $desplegada, encabezado:TextField("Buscar", text: $a).padding(.bottom).textFieldStyle(SearchTextFieldStyle(a: $a))
.padding(.horizontal, 15)){
Color.gray
}
}
}
}
@haydgately
Copy link

@RubeDEV Could you include ContentView and how to call this and use it? I cannot understand the spanish to this, thanks

@RubeDEV
Copy link
Author

RubeDEV commented May 12, 2020

In fact there was an example at the end of TarjetaDesplegada.swift. Anyway I provided ContentView.swift with simple explanation about use.

@haydgately
Copy link

Thanks @RubeDEV - is SearchTextFieldStyle a library you have imported? I cannot get it to work due to "Use of unresolved identifier 'SearchTextFieldStyle' " - Thanks

@RubeDEV
Copy link
Author

RubeDEV commented May 12, 2020

Thanks @RubeDEV - is SearchTextFieldStyle a library you have imported? I cannot get it to work due to "Use of unresolved identifier 'SearchTextFieldStyle' " - Thanks

It is a custom style, I’m adding it in case you want to try it out. Let me know how are you using it and if you could figure new ways of improving this. It’s still under development.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment