Neomorfismo usando SwiftUI. Parte 1

Saludo, Khabrovites! En previsión del lanzamiento del curso avanzado "IOS Developer", hemos preparado otra traducción interesante.




El diseño no mórfico es quizás la tendencia más interesante de los últimos meses, aunque, en verdad, Apple lo usó como motivo de diseño en WWDC18. En este artículo, veremos cómo puede implementar un diseño no mórfico utilizando SwiftUI, por qué es posible que desee hacer esto y, lo más importante, cómo podemos refinar este diseño para aumentar su accesibilidad.
Importante : el neomorfismo, también llamado a veces neuromorfismo, tiene graves consecuencias para la accesibilidad, por lo que a pesar de la tentación de leer la primera parte de este artículo y omitir el resto, le insto a que lea el artículo hasta el final y estudie las ventajas y desventajas para que pueda ver la imagen completa. .


Fundamentos del neomorfismo


Antes de pasar al código, quiero describir brevemente dos principios básicos de esta dirección en el diseño, ya que serán relevantes a medida que avancemos:

  1. El neomorfismo usa reflejos y sombras para determinar las formas de los objetos en la pantalla.
  2. El contraste tiende a disminuir; no se utilizan completamente blanco o negro, lo que le permite resaltar luces y sombras.

El resultado final es una apariencia que recuerda al "plástico extruido", un diseño de interfaz de usuario que ciertamente se ve fresco e interesante sin chocar con sus ojos. No puedo dejar de repetir una vez más que reducir el contraste y usar sombras para resaltar formas afecta seriamente la accesibilidad, y volveremos a esto más adelante.

Sin embargo, sigo pensando que el tiempo dedicado a aprender sobre el neomorfismo en SwiftUI lo vale, incluso si no lo usa en sus propias aplicaciones, es como un código escribiendo kata para ayudar a perfeccionar sus habilidades.

Muy bien, suficiente conversación inactiva: pasemos al código.

Construyendo un mapa no mórfico


El punto de partida más simple es crear un mapa no mórfico: un rectángulo redondeado que contendrá alguna información. A continuación, veremos cómo podemos transferir estos principios a otras partes de SwiftUI.

Comencemos creando un nuevo proyecto de iOS usando la plantilla de aplicación Single View. Asegúrate de usar SwiftUI para la interfaz de usuario y luego nombra el proyecto Neumorphism.

Consejo: si tiene acceso a la vista previa de SwiftUI en Xcode, le recomiendo que lo active de inmediato, será mucho más fácil para usted experimentar.

Comenzaremos definiendo un color que represente un tono cremoso. Esto no es gris puro, sino un tono muy sutil que agrega un poco de calidez o frescura a la interfaz. Puede agregarlo al directorio de activos si lo desea, pero ahora es más fácil hacerlo en código.

Agregue esto Colorfuera de la estructura ContentView:

extension Color {
    static let offWhite = Color(red: 225 / 255, green: 225 / 255, blue: 235 / 255)
}

Sí, es casi blanco, pero es lo suficientemente oscuro como para que el blanco real parezca un resplandor cuando lo necesitamos.

Ahora podemos llenar el cuerpo ContentViewproporcionándolo ZStack, que ocupa toda la pantalla, usando nuestro nuevo color casi blanco para llenar todo el espacio:

struct ContentView: View {
    var body: some View {
        ZStack {
            Color.offWhite
        }
        .edgesIgnoringSafeArea(.all)
    }
}

Para representar nuestro mapa, utilizaremos un rectángulo redondeado en la resolución de 300x300 para que quede hermoso y claro en la pantalla. Entonces agregue esto a ZStack, debajo del color:

RoundedRectangle(cornerRadius: 25)
    .frame(width: 300, height: 300)

Por defecto será negro, pero para la implementación del neomorfismo queremos reducir drásticamente el contraste, por lo que lo reemplazaremos con el mismo color que usamos para el fondo, de hecho haciendo invisible la forma.

Así que cámbialo así:

RoundedRectangle(cornerRadius: 25)
    .fill(Color.offWhite)
    .frame(width: 300, height: 300)

Un punto importante: determinamos la forma usando sombras, una oscura y una clara, como si la luz proyectara rayos desde la esquina superior izquierda de la pantalla.

SwiftUI nos permite aplicar modificadores varias veces, lo que facilita la implementación del neomorfismo. Agregue los siguientes dos modificadores a su rectángulo redondeado:

.shadow(color: Color.black.opacity(0.2), radius: 10, x: 10, y: 10)
.shadow(color: Color.white.opacity(0.7), radius: 10, x: -5, y: -5)

Representan el desplazamiento de la sombra oscura en la esquina inferior derecha y el desplazamiento de la sombra clara en la esquina superior izquierda. La sombra clara es visible porque usamos un fondo casi blanco, y ahora el mapa se vuelve visible.

Hemos escrito unas pocas líneas de código, pero ya tenemos un mapa no mórfico. ¡Espero que acepte que SwiftUI sorprendentemente facilita el proceso!



Crear un botón simple no mórfico


De todos los elementos de una IU, el neomorfismo representa un riesgo bastante bajo para las tarjetas: si la IU dentro de sus tarjetas es clara, la tarjeta puede no tener un borde claro, y esto no afectará la accesibilidad. Los botones son otra cosa, porque están diseñados para interactuar, por lo que reducir su contraste puede hacer más daño que bien.

Tratemos esto creando nuestro propio estilo de botón, ya que esta es la forma en que SwiftUI permite que las configuraciones de botones se distribuyan en muchos lugares. Esto es mucho más conveniente que agregar muchos modificadores a cada botón que cree; simplemente podemos definir el estilo una vez y usarlo en muchos lugares.

Vamos a definir un estilo de botón que en realidad estará vacío: SwiftUI nos dará la etiqueta para el botón, que puede ser texto, imagen u otra cosa, y lo enviaremos de regreso sin cambios.

Agregue esta estructura en algún lugar afuera ContentView:

struct SimpleButtonStyle: ButtonStyle {
    func makeBody(configuration: Self.Configuration) -> some View {
        configuration.label
    }
}

Este configuration.labeles el contenido del botón, y pronto agregaremos algo más. Primero, definamos un botón que lo use para que pueda ver cómo evoluciona el diseño:

Button(action: {
    print("Button tapped")
}) {
    Image(systemName: "heart.fill")
        .foregroundColor(.gray)
}
.buttonStyle(SimpleButtonStyle())

No verá nada especial en la pantalla, pero podemos solucionarlo agregando nuestro efecto no mórfico al estilo del botón. Esta vez no usaremos un rectángulo redondeado, porque para iconos simples el círculo es mejor, pero necesitamos agregar algo de sangría para que el área de clic del botón sea grande y hermosa.
Cambie su método makeBody()agregando un poco de sangría y luego colocando nuestro efecto no mórfico como fondo para el botón:

configuration.label
    .padding(30)
    .background(
        Circle()
            .fill(Color.offWhite)
            .shadow(color: Color.black.opacity(0.2), radius: 10, x: 10, y: 10)
            .shadow(color: Color.white.opacity(0.7), radius: 10, x: -5, y: -5)
    )



Esto nos acerca lo suficiente al efecto deseado, pero si ejecuta la aplicación, verá que en la práctica el comportamiento aún no es perfecto: el botón no responde visualmente cuando se presiona, lo que se ve raro.

Para solucionar esto, debemos leer la propiedad configuration.isPresseddentro de nuestro estilo de botón personalizado, que indica si el botón está presionado o no. Podemos usar esto para mejorar nuestro estilo para dar una indicación visual de si se presiona el botón.

Comencemos con uno simple: usaremos Groupbotones para el fondo, luego verificaremos configuration.isPressedy devolveremos un círculo plano si se presiona el botón, o nuestro círculo oscuro actual de lo contrario:

configuration.label
    .padding(30)
    .background(
        Group {
            if configuration.isPressed {
                Circle()
                    .fill(Color.offWhite)
            } else {
                Circle()
                    .fill(Color.offWhite)
                    .shadow(color: Color.black.opacity(0.2), radius: 10, x: 10, y: 10)
                    .shadow(color: Color.white.opacity(0.7), radius: 10, x: -5, y: -5)
            }
        }
    )

Dado que el isPressedcírculo con color casi blanco se usa en el estado , hace que nuestro efecto sea invisible cuando se presiona el botón.

Advertencia: debido a la forma en que SwiftUI calcula las áreas tapables, involuntariamente hicimos que el área de clic para nuestro botón fuera muy pequeña; ahora debe tocar la imagen en sí, y no el diseño unomórfico a su alrededor. Para solucionar esto, agregue un modificador .contentShape(Circle())inmediatamente después .padding(30), forzando a SwiftUI a usar todo el espacio disponible.

Ahora podemos crear el efecto de la concavidad artificial volteando la sombra, copiando dos modificadores shadowdel efecto base, intercambiando los valores X e Y para blanco y negro, como se muestra aquí:

if configuration.isPressed {
    Circle()
        .fill(Color.offWhite)
        .shadow(color: Color.black.opacity(0.2), radius: 10, x: -5, y: -5)
        .shadow(color: Color.white.opacity(0.7), radius: 10, x: 10, y: 10)
} else {
    Circle()
        .fill(Color.offWhite)
        .shadow(color: Color.black.opacity(0.2), radius: 10, x: 10, y: 10)
        .shadow(color: Color.white.opacity(0.7), radius: 10, x: -5, y: -5)
}

Estima el resultado.



Crea sombras internas para hacer clic en un botón


Nuestro código actual, en principio, ya funciona, pero las personas interpretan el efecto de manera diferente: algunos lo ven como un botón cóncavo, otros ven que el botón todavía no está presionado, solo que la luz proviene de un ángulo diferente.

La idea de la mejora es crear una sombra interna que simule el efecto de presionar el botón hacia adentro. Esto no es parte del kit estándar SwiftUI, pero podemos implementarlo con bastante facilidad.

Crear una sombra interna requiere dos gradientes lineales, y serán solo el primero de muchos gradientes internos que usaremos en este artículo, por lo que agregaremos inmediatamente una pequeña extensión auxiliar para LinearGradientsimplificar la creación de gradientes estándar:

extension LinearGradient {
    init(_ colors: Color...) {
        self.init(gradient: Gradient(colors: colors), startPoint: .topLeading, endPoint: .bottomTrailing)
    }
}

Con esto, simplemente podemos proporcionar una lista variable de colores para recuperar su gradiente lineal en la dirección diagonal.

Ahora sobre el punto importante: en lugar de agregar dos sombras invertidas a nuestro círculo presionado, vamos a superponer un nuevo círculo con un desenfoque (trazo) y luego aplicar otro círculo con un degradado como máscara. Esto es un poco más complicado, pero déjenme explicarlo poco a poco:

  • Nuestro círculo base es nuestro círculo actual con un efecto neomórfico, lleno de un color casi blanco.
  • Colocamos un círculo encima de él, enmarcado por un marco gris, y un poco borroso para suavizar sus bordes.
  • Luego aplicamos una máscara con otro círculo a este círculo superpuesto en la parte superior, esta vez lleno de un degradado lineal.

Cuando aplica una vista como máscara para otra, SwiftUI usa el canal alfa de la máscara para determinar qué debe mostrarse en la vista base.

Entonces, si dibujamos un trazo gris borroso, y luego lo enmascaramos con un degradado lineal de negro a transparente, el trazo borroso será invisible en un lado y aumentará gradualmente en el otro, obtendremos un gradiente interno suave. Para que el efecto sea más pronunciado, podemos cambiar ligeramente los círculos sombreados en ambas direcciones. Después de experimentar un poco, descubrí que dibujar una sombra clara con una línea más gruesa que una oscura ayuda a maximizar el efecto.

Recuerde que se usan dos sombras para crear una sensación de profundidad en el neomorfismo: una clara y otra oscura, por lo que agregaremos este efecto de la sombra interna dos veces con diferentes colores.

Cambia el círculo de la configuration.isPresssiguiente manera:

Circle()
    .fill(Color.offWhite)
    .overlay(
        Circle()
            .stroke(Color.gray, lineWidth: 4)
            .blur(radius: 4)
            .offset(x: 2, y: 2)
            .mask(Circle().fill(LinearGradient(Color.black, Color.clear)))
    )
    .overlay(
        Circle()
            .stroke(Color.white, lineWidth: 8)
            .blur(radius: 4)
            .offset(x: -2, y: -2)
            .mask(Circle().fill(LinearGradient(Color.clear, Color.black)))
    )

Si ejecuta la aplicación nuevamente, verá que el efecto de presionar un botón es mucho más pronunciado y se ve mejor.



Sobre esto, la primera parte de la traducción llegó a su fin. En los próximos días publicaremos la continuación, y ahora los invitamos a conocer más sobre el próximo curso .

All Articles