Neomorfismo usando SwiftUI. Parte 2: ¿Qué se puede hacer con la accesibilidad?

¡Hola a todos! En previsión del lanzamiento del curso avanzado "IOS Developer", publicamos una traducción de la segunda parte del artículo sobre el neomorfismo utilizando SwiftUI ( lea la primera parte ).





Tema oscuro


Antes de comenzar a comprender cómo podemos mejorar la disponibilidad del neomorfismo, veamos cómo podemos trabajar con el efecto para crear otros estilos interesantes.

Primero agregue dos colores más a la extensión Colorpara que tengamos a mano varios valores constantes:

static let darkStart = Color(red: 50 / 255, green: 60 / 255, blue: 65 / 255)
static let darkEnd = Color(red: 25 / 255, green: 25 / 255, blue: 30 / 255)

Podemos usarlos como fondo para ContentViewreemplazar el existente Color.white, como se muestra aquí:

var body: some View {
    ZStack {
        LinearGradient(Color.darkStart, Color.darkEnd)

El nuestro SimpleButtonStyleahora parece fuera de lugar, porque impone una estilización brillante sobre un fondo oscuro. Entonces, vamos a crear un nuevo estilo oscuro que funcione mejor aquí, pero esta vez lo dividiremos en dos partes: una vista de fondo que podemos usar en cualquier lugar, y un estilo de botón que lo envuelve con los modificadores de relleno y contenido. Esto nos dará más flexibilidad, como verá más adelante.

La nueva vista de fondo que vamos a agregar nos permitirá especificar cualquier forma para nuestro efecto visual, por lo que ya no estamos unidos a los círculos. También hará un seguimiento de si cóncavar nuestro efecto convexo (hacia adentro o hacia afuera), dependiendo de la propiedad isHighlightedque podamos cambiar desde el exterior.

Comenzaremos con el más simple, usando un enfoque modificado de cambio de sombra para obtener un efecto cóncavo. Agregue esta estructura:

struct DarkBackground<S: Shape>: View {
    var isHighlighted: Bool
    var shape: S

    var body: some View {
        ZStack {
            if isHighlighted {
                shape
                    .fill(Color.darkEnd)
                    .shadow(color: Color.darkStart, radius: 10, x: 5, y: 5)
                    .shadow(color: Color.darkEnd, radius: 10, x: -5, y: -5)

            } else {
                shape
                    .fill(Color.darkEnd)
                    .shadow(color: Color.darkStart, radius: 10, x: -10, y: -10)
                    .shadow(color: Color.darkEnd, radius: 10, x: 10, y: 10)
            }
        }
    }
}

La modificación es que cuando se presiona el botón, el tamaño de la sombra disminuye: utilizamos una distancia de 5 puntos en lugar de 10.

Luego, podemos envolverlo con la ayuda de DarkButtonStylerelleno y contentShape, como se muestra aquí:

struct DarkButtonStyle: ButtonStyle {
    func makeBody(configuration: Self.Configuration) -> some View {
        configuration.label
            .padding(30)
            .contentShape(Circle())
            .background(
                DarkBackground(isHighlighted: configuration.isPressed, shape: Circle())
            )
    }
}

Finalmente, podemos aplicar esto a nuestro botón ContentViewcambiándolo ButtonStyle():

.buttonStyle(DarkButtonStyle())

Veamos qué sucedió: aunque no tenemos mucho código, creo que el resultado se ve lo suficientemente bueno.



Algunos experimentos


Ahora es el momento de experimentar con el efecto, porque te ayudará a comprender mejor de qué es capaz SwiftUI específicamente.

Por ejemplo, podríamos crear un botón convexo suave agregando un degradado lineal y volteándolo cuando se presiona:

if isHighlighted {
    shape
        .fill(LinearGradient(Color.darkEnd, Color.darkStart))
        .shadow(color: Color.darkStart, radius: 10, x: 5, y: 5)
        .shadow(color: Color.darkEnd, radius: 10, x: -5, y: -5)

} else {
    shape
        .fill(LinearGradient(Color.darkStart, Color.darkEnd))
        .shadow(color: Color.darkStart, radius: 10, x: -10, y: -10)
        .shadow(color: Color.darkEnd, radius: 10, x: 10, y: 10)
}

Si ejecuta esto, verá que el botón se anima suavemente hacia arriba y hacia abajo cuando se presiona y suelta. Creo que la animación es un poco molesta, por lo que recomiendo desactivarla agregando este modificador al método makeBody()desde DarkButtonStyle, después del modificador presente allí background():

.animation(nil)



Este efecto del botón de amortiguación es encantador, pero si planea usarlo, le aconsejaría que pruebe los siguientes tres cambios para que el botón se destaque un poco más.

En primer lugar, a pesar de que esto contradice el principio de bajo contraste del diseño neomórfico, reemplazaría el ícono gris por uno blanco para que se destaque. Entonces, ContentViewnecesitarías lo siguiente:

Image(systemName: "heart.fill")
    .foregroundColor(.white)

En segundo lugar, si agrega una superposición para el botón en el estado presionado, esto no solo hará que se vea más como un botón físico real presionado de manera uniforme, sino que también ayudará a distinguir su estado presionado del presionado.

Para implementar esto, es necesario insertar el modificador overlay()después fill()cuando isHighlightedes cierto, como en este caso:

if isHighlighted {
    shape
        .fill(LinearGradient(Color.darkEnd, Color.darkStart))
        .overlay(shape.stroke(LinearGradient(Color.darkStart, Color.darkEnd), lineWidth: 4))
        .shadow(color: Color.darkStart, radius: 10, x: 5, y: 5)
        .shadow(color: Color.darkEnd, radius: 10, x: -5, y: -5)



Para lograr una apariencia aún más nítida, de lo shadow()contrario , puede eliminar dos modificadores para el estado presionado que se centran en la superposición.

En tercer lugar, también puede agregar una superposición al estado no comprimido, solo para marcar que es un botón. Póngalo inmediatamente después fill(), así:

} else {
    shape
        .fill(LinearGradient(Color.darkStart, Color.darkEnd))
        .overlay(shape.stroke(Color.darkEnd, lineWidth: 4))
        .shadow(color: Color.darkStart, radius: 10, x: -10, y: -10)
        .shadow(color: Color.darkEnd, radius: 10, x: 10, y: 10)
}



Agregar un estilo de interruptor


Una de las ventajas de separar un estilo de botón de un estilo de fondo no mórfico es que ahora podemos agregar un estilo de interruptor con el mismo efecto. Esto significa crear una nueva estructura compatible con el protocolo ToggleStyleque sea similar a ButtonStyle, excepto que:

  1. Necesitamos leer configuration.isOnpara determinar si el interruptor está activado.
  2. Necesitamos proporcionar un botón para manejar el acto de cambiar, o al menos algo así onTapGesture()o algo así .

Agregue esta estructura a su proyecto:

struct DarkToggleStyle: ToggleStyle {
    func makeBody(configuration: Self.Configuration) -> some View {
        Button(action: {
            configuration.isOn.toggle()
        }) {
            configuration.label
                .padding(30)
                .contentShape(Circle())
        }
        .background(
            DarkBackground(isHighlighted: configuration.isOn, shape: Circle())
        )
    }
}

Queremos poner uno de ellos ContentViewpara que pueda probarlo usted mismo, así que comience agregando esta propiedad:

@State private var isToggled = false

Luego envuelva el botón existente VStackcon un espacio igual a 40 y colóquelo a continuación:

Toggle(isOn: $isToggled) {
    Image(systemName: "heart.fill")
        .foregroundColor(.white)
}
.toggleStyle(DarkToggleStyle())

Su estructura ContentViewdebería verse así:

struct ContentView: View {
    @State private var isToggled = false

    var body: some View {
        ZStack {
            LinearGradient(Color.darkStart, Color.darkEnd)

            VStack(spacing: 40) {
                Button(action: {
                    print("Button tapped")
                }) {
                    Image(systemName: "heart.fill")
                        .foregroundColor(.white)
                }
                .buttonStyle(DarkButtonStyle())

                Toggle(isOn: $isToggled) {
                    Image(systemName: "heart.fill")
                        .foregroundColor(.white)
                }
                .toggleStyle(DarkToggleStyle())
            }
        }
        .edgesIgnoringSafeArea(.all)
    }
}

Y eso es todo: ¡aplicamos nuestro diseño neomórfico común en dos lugares!

Mejora de accesibilidad


Tuvimos suficiente tiempo para jugar con varios estilos neomórficos, pero ahora quiero detenerme en los problemas de este diseño: una falta extrema de contraste significa que los botones y otros controles importantes no son suficientes para destacarse de su entorno, lo que dificulta el uso de nuestras aplicaciones para personas con Visión imperfecta.

Este es el momento en el que observo algunos malentendidos, así que quiero decir algunas cosas de antemano:

  1. Sí, entiendo que los botones estándar de Apple se parecen al texto azul y, por lo tanto, no se parecen a los botones familiares, al menos a primera vista, pero tienen una alta relación de contraste.
  2. « , , » — — « », , , - .
  3. - SwiftUI, , VoiceOver Apple.

Ya ha visto cómo cambiamos el ícono gris a blanco para obtener una mejora de contraste instantánea, pero los botones e interruptores aún necesitan mucho más contraste si desea que sean más accesibles.

Entonces, vamos a considerar qué cambios podríamos hacer para que los elementos realmente se destaquen.

Primero, me gustaría que agregue dos nuevos colores a nuestra extensión:

static let lightStart = Color(red: 60 / 255, green: 160 / 255, blue: 240 / 255)
static let lightEnd = Color(red: 30 / 255, green: 80 / 255, blue: 120 / 255)

En segundo lugar, duplica el existente DarkBackgroundy nombra la copia ColorfulBackground. Lo abordaremos en un momento, pero de nuevo, primero tenemos que hacer algo de preparación.

Tercero, duplique el estilo oscuro del botón y cambie, cámbieles el nombre por ColorfulButtonStyley ColorfulToggleStyle, y luego haga que usen el nuevo ColorfulBackgroundcomo fondo.

Entonces deberían verse así:

struct ColorfulButtonStyle: ButtonStyle {
    func makeBody(configuration: Self.Configuration) -> some View {
        configuration.label
            .padding(30)
            .contentShape(Circle())
            .background(
                ColorfulBackground(isHighlighted: configuration.isPressed, shape: Circle())
            )
            .animation(nil)
    }
}

struct ColorfulToggleStyle: ToggleStyle {
    func makeBody(configuration: Self.Configuration) -> some View {
        Button(action: {
            configuration.isOn.toggle()
        }) {
            configuration.label
                .padding(30)
                .contentShape(Circle())
        }
        .background(
            ColorfulBackground(isHighlighted: configuration.isOn, shape: Circle())
        )
    }
}

Y finalmente, edite el botón y cámbielo ContentViewpara usar el nuevo estilo:

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

Toggle(isOn: $isToggled) {
    Image(systemName: "heart.fill")
        .foregroundColor(.white)
}
.toggleStyle(ColorfulToggleStyle())

Puede ejecutar la aplicación si lo desea, pero eso no tiene sentido: en realidad no ha cambiado.

Para traer nuestra versión colorida de la vida, vamos a cambiar los modificadores fill()y overlay()los estados presionados y no presionados. Por lo tanto, cuando isHighlightedverdadera, cambiar darkStarttanto darkEnda lightStarty lightEnd, de esta manera:

if isHighlighted {
    shape
        .fill(LinearGradient(Color.lightEnd, Color.lightStart))
        .overlay(shape.stroke(LinearGradient(Color.lightStart, Color.lightEnd), lineWidth: 4))
        .shadow(color: Color.darkStart, radius: 10, x: 5, y: 5)
        .shadow(color: Color.darkEnd, radius: 10, x: -5, y: -5)

Si vuelve a ejecutar la aplicación, verá que ya ha mejorado significativamente: el estado presionado ahora tiene un color azul brillante, por lo que queda claro cuando se presionan los botones y los interruptores están activos. Pero podemos hacer algo más: podemos agregar el mismo color alrededor del botón cuando no se presiona, lo que ayuda a llamar la atención.



Para hacer esto, cambie el overlay()estado no estresado existente a esto:

.overlay(shape.stroke(LinearGradient(Color.lightStart, Color.lightEnd), lineWidth: 4))


Entonces, el estilo del botón terminado debería verse así:

ZStack {
    if isHighlighted {
        shape
            .fill(LinearGradient(Color.lightEnd, Color.lightStart))
            .overlay(shape.stroke(LinearGradient(Color.lightStart, Color.lightEnd), lineWidth: 4))
            .shadow(color: Color.darkStart, radius: 10, x: 5, y: 5)
            .shadow(color: Color.darkEnd, radius: 10, x: -5, y: -5)
    } else {
        shape
            .fill(LinearGradient(Color.darkStart, Color.darkEnd))
            .overlay(shape.stroke(LinearGradient(Color.lightStart, Color.lightEnd), lineWidth: 4))
            .shadow(color: Color.darkStart, radius: 10, x: -10, y: -10)
            .shadow(color: Color.darkEnd, radius: 10, x: 10, y: 10)
    }
}

Ahora ejecute la aplicación nuevamente y verá que ha aparecido un anillo azul alrededor de los botones e interruptores, y cuando se hace clic en él se llena de azul, es mucho más accesible.



Conclusión


En la práctica, no tendrá varios estilos de botones diferentes en un proyecto, al menos si no desea crear un dolor de cabeza para sus usuarios. Pero este es un espacio interesante para la experimentación, y espero que capten esta idea de mi artículo, porque pueden crear efectos realmente hermosos sin romper la espalda.

He dicho repetidamente que siempre debe controlar la disponibilidad de su aplicación, y eso significa más que solo asegurarse de que VoiceOver funcione con su interfaz de usuario. Asegúrese de que sus botones se vean interactivos, asegúrese de que sus etiquetas e iconos de texto tengan una relación de contraste suficiente con el fondo (al menos 4.5: 1, pero tienden a 7: 1), y asegúrese de que sus áreas clicables sean cómodas y grande (al menos 44x44 píxeles).

Y por el amor de Dios, usa el diseño neomórfico para experimentar y ampliar tu conocimiento de SwiftUI, pero nunca olvides que si sacrificas la usabilidad por una nueva tendencia de diseño de moda, no ganaste nada.

Puede obtener el código fuente completo para este proyecto en GitHub .

Lea la primera parte.



Aprende más sobre el curso.



All Articles