Last active
May 13, 2020 14:33
-
-
Save RubeDEV/c63d1383dedfccd170baa72ed810732d to your computer and use it in GitHub Desktop.
Tarjeta Deslizable (Slide-Over Card)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. | |
*/ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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 | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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 | |
} | |
} | |
} | |
} |
In fact there was an example at the end of TarjetaDesplegada.swift. Anyway I provided ContentView.swift with simple explanation about use.
Thanks @RubeDEV - is SearchTextFieldStyle a library you have imported? I cannot get it to work due to "Use of unresolved identifier 'SearchTextFieldStyle' " - Thanks
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
@RubeDEV Could you include ContentView and how to call this and use it? I cannot understand the spanish to this, thanks