El protocolo Animatable nos permite de una manera muy sencilla trabajar propiedades que cambian de valor para animar su movimiento desde la vista. Es un tipo que describe como animar una propiedad de la vista.
Disponible desde iOS 13
Si bien el protocolo no exige que se añada la propiedad animatableData, ésta nos permitirá asignar el nuevo valor cuando el estado cambie a la propiedad (en el ejemplo isOn) que usaremos en nuestro struct como referencia del valor.
//..
var isOn: Bool
var animatableData: Bool {
get { isOn }
set { isOn = newValue }
}
//..
Vamos a crear animaciones de nuestros Textos, Botones y Shapes, pasando los valores con el operador ternario para cambiar el estado.
Para el ejemplo al pulsar el botón cambiará de tamaño y posición de vertical a horizontal. Necesitaremos el tamaño, los grados para el rotationEffect y a que punto estará anclado, al que asignaremos un valor por defecto.
struct ViewTransition {
var size: Double
var degrees: Double
var anchor: UnitPoint = .bottomLeading
}
Ahora crearemos otro struct que se conforme con Animatable
y ViewModifier
para poder utilizarlo como modificador de la View, al que llamaremos TextTransition
struct TextTransition: Animatable, ViewModifier {
var textTransition: TextTransition
var animatableData: TextTransition {
get { textTransition }
set { textTransition = newValue }
}
func body(content: Content) -> some View {
content
.font(.system(size: textTransition.size))
.rotationEffect(.degrees(textTransition.degrees), anchor: textTransition.anchor)
}
}
Finalmente hacer una extension de View
y podamos utilizar la función que incluya el modificador.
extension View {
func textTransition(_ viewTransition: ViewTransition) -> some View {
modifier(TextTransition(textTransition: viewTransition))
}
}
Para añadir la animación utilizamos el método withAnimation(.spring)
, (disponible desde iOS13), y cambiaremos el estado de la variable isOn
, con la que el operador ternario asignará un valor y otro, además en la propiedad @State
var textTransition
, definiremos los valores iniciales.
struct AnimatableButton: View {
@State var textTransition = ViewTransition(size: 20, degrees: 90)
@State var isOn = false
var body: some View {
Button("Hello World!"){
isOn.toggle()
withAnimation(.spring) {
textTransition = ViewTransition(size: isOn ? 40 : 20, degrees: isOn ? 0 : 90)
}
}
.textTransition(textTransition)
.buttonStyle(BorderedProminentButtonStyle()
}
}
Colores
Podemos también animar el color, para ello crearemos un struct con los valores que necesitamos.
struct GradientTransition {
var gradient: Gradient = .rainbow
var startPoint: UnitPoint
var endPoint: UnitPoint
}
Lo crearemos como un ButtonStyle
, lo que nos permitirá elegirlo desde como un estilo de botón.
Para ello crearemos un struct
ButtonGradient
con la propiedad gradientTransition
, y la propiedad animatableData
que asignará el valor.
Añadiré también un inicializador para omitir el valor del argumento, y no sea tan redundante su llamada, y la función makeBody que dará al botón su diseño.
struct ButtonGradient: Animatable, ButtonStyle {
var gradientTransition: GradientTransition
var animatableData: GradientTransition {
get { gradientTransition }
set { gradientTransition = newValue }
}
init(_ gradientTransition: GradientTransition) {
self.gradientTransition = gradientTransition
}
func makeBody(configuration: Configuration) -> some View {
configuration.label
.fontStyle(.headline, bold: configuration.isPressed)
.padding(10)
.background(
RoundedRectangle(cornerRadius: 12)
.foregroundStyle(.linearGradient(gradientTransition.gradient, startPoint: gradientTransition.startPoint, endPoint: gradientTransition.endPoint))
)
.shadow(color: configuration.isPressed ? .clear : .backgroundCC, radius: 2,x:0,y:0)
.scaleEffect(configuration.isPressed ? 0.90 : 1)
}
}
Lo anterior aún se podría componentizar un poco más, como haremos con la extensión de esa vista, al añadir una función que añada la animación.
extension AnimatableGradientButton {
func withGradientAnimation() {
withAnimation(.spring) {
gradientTransition = GradientTransition(startPoint: isOn ? .bottomLeading : .topTrailing, endPoint: isOn ? .topLeading : .bottomTrailing)
}
}
}
Nuestra botón, incluirá las propiedades gradientTransition, y la propiedad isOn
que capturará el valor. Podremos utilizar el buttonStyle
con nuestro estilo ButtonGradient
.
struct AnimatableGradientButton: View {
@State var gradientTransition = GradientTransition(startPoint: .bottomLeading, endPoint: .topLeading)
@State var isOn = false
var body: some View {
Button("Hello World!") {
isOn.toggle()
withGradientAnimation()
}
.buttonStyle(ButtonGradient(gradientTransition))
}
}
Shapes
Con los Shapes, podemos crear animaciones si parametrizamos el path, la función que conforma el protocolo. Crearemos un Rectángulo que tendrá animaciones en los 4 lados, para ello utilizaremos la función addQuadCurve
, que utiliza las curvas de Bézier.
struct JellyRectangle: Shape {
var offset: CGFloat = 0.0
var animatableData: CGFloat {
get { offset }
set { offset = newValue }
}
func path(in rect: CGRect) -> Path {
var path = Path()
//top
path.move(to: CGPoint(x: 0, y: 0))
path.addQuadCurve(to: CGPoint(x: rect.maxX, y: 0),
control: CGPoint(x: rect.midX, y: rect.minY + offset))
//trailing
path.addQuadCurve(to: CGPoint(x: rect.maxX, y: rect.maxY),
control: CGPoint(x: rect.maxX - offset, y: rect.maxY))
//bottom
path.addQuadCurve(to: CGPoint(x: 0, y: rect.maxY),
control: CGPoint(x: rect.midX, y: rect.maxY - offset))
//leading
path.addQuadCurve(to: CGPoint(x: 0, y: 0),
control: CGPoint(x: rect.minX + offset, y: rect.midY + offset))
return path
}
}
El offtset sirve de punto de control de la curva de Bézier, y añadimos una animación que se continuará repitiéndose al mostrarse la vista.
struct AnimatableRectangle: View {
@State var offset = 0.0
@State var isOn = false
var body: some View {
JellyRectangle(offset: offset)
.fill(LinearGradient(gradient: .rainbow, startPoint: .bottomTrailing, endPoint: .topLeading))
.onAppear {
withAnimation(.easeInOut(duration: 0.2).repeatForever()) {
offset = 20.0
}
}
}
}
¿Quieres recibir posts, cheatCodes, enlaces y katas en Swift para practicar?
Quincenalmente recibirás en tu correo electrónico la newsletter, solo hace falta tu correo electrónico.