Neomorfismo usando SwiftUI. Parte 2: O que pode ser feito com acessibilidade?

Olá a todos! Em antecipação ao lançamento do curso avançado “IOS Developer”, publicamos uma tradução da segunda parte do artigo sobre neomorfismo usando o SwiftUI ( leia a primeira parte ).





Tema escuro


Antes de começarmos a entender como podemos melhorar a disponibilidade do neomorfismo, vamos ver como podemos trabalhar com o efeito para criar outros estilos interessantes.

Primeiro adicione mais duas cores à extensão, Colorpara que tenhamos vários valores constantes à mão:

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 usá-los como pano de fundo ContentView, substituindo o existente Color.white, conforme mostrado aqui:

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

O nosso SimpleButtonStyleagora parece fora de lugar, porque impõe uma estilização brilhante em um fundo escuro. Então, criaremos um novo estilo escuro que funcione melhor aqui, mas desta vez vamos dividi-lo em duas partes: uma visualização em segundo plano que podemos usar em qualquer lugar, e um estilo de botão que o envolva com os modificadores de preenchimento e contentShape. Isso nos dará mais flexibilidade, como você verá mais adiante.

A nova visualização de plano de fundo que adicionaremos nos permitirá especificar qualquer forma para nosso efeito visual, para que não fiquemos mais apegados a círculos. Ele também acompanhará se côncava nosso efeito convexo (interno ou externo), dependendo da propriedade isHighlightedque podemos mudar do lado de fora.

Começaremos com o mais simples, usando uma abordagem de inversão de sombra modificada para obter um efeito côncavo. Adicione esta estrutura:

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)
            }
        }
    }
}

A modificação é que, quando o botão é pressionado, o tamanho da sombra diminui - usamos uma distância de 5 pontos em vez de 10.

Em seguida, podemos envolvê-lo com a ajuda de DarkButtonStylepadding e contentShape, como mostrado aqui:

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 isso ao nosso botão ContentViewalterando-o ButtonStyle():

.buttonStyle(DarkButtonStyle())

Vamos ver o que aconteceu - embora não tenhamos muito código, acho que o resultado parece bom o suficiente.



Algumas experiências


Agora é a hora de experimentar o efeito, porque ele ajudará você a entender melhor do que o SwiftUI é especificamente capaz.

Por exemplo, podemos criar um botão convexo suave adicionando um gradiente linear a ele e girá-lo quando pressionado:

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)
}

Se você executar isso, verá que o botão anima suavemente para cima e para baixo quando pressionado e solto. Eu acho que a animação é um pouco perturbadora, por isso recomendo desativá-la adicionando esse modificador ao método makeBody()from DarkButtonStyle, após o modificador presente lá background():

.animation(nil)



Esse efeito do botão de almofada é encantador, mas se você planeja usá-lo, aconselho a tentar as três alterações a seguir para destacar o botão um pouco mais.

Em primeiro lugar, apesar do fato de que isso contradiz o princípio de baixo contraste do design neomórfico, eu substituiria o ícone cinza por um ícone branco para destacá-lo. Então, ContentViewvocê precisaria do seguinte:

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

Em segundo lugar, se você adicionar sobreposição para o botão no estado pressionado, isso não apenas fará com que pareça mais com um botão físico real pressionado uniformemente, mas também ajudará a distinguir seu estado pressionado do não pressionado.

Para implementar isso, você precisa inserir o modificador overlay()depois fill()quando isHighlightedé verdade, como aqui:

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 obter uma aparência ainda mais nítida, você pode remover dois modificadores shadow()para o estado pressionado que se concentram na sobreposição.

Em terceiro lugar, você também pode adicionar sobreposição ao estado não compactado, apenas para marcar que é um botão. Coloque-o imediatamente depois fill(), assim:

} 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)
}



Adicionando um estilo de switch


Uma das vantagens de separar um estilo de botão de um estilo de fundo não-mórfico é que agora podemos adicionar um estilo de opção usando o mesmo efeito. Isso significa criar uma nova estrutura compatível com protocolo ToggleStylesemelhante a ButtonStyle, exceto que:

  1. Precisamos ler configuration.isOnpara determinar se o interruptor está ativado.
  2. Precisamos fornecer um botão para lidar com o ato de mudar, ou pelo menos algo assim onTapGesture()ou algo assim .

Adicione esta estrutura ao seu projeto:

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 colocar um deles ContentViewpara que você possa testá-lo, então comece adicionando esta propriedade:

@State private var isToggled = false

Em seguida, enrole o botão existente VStackcom espaçamento igual a 40 e coloque-o abaixo:

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

Sua estrutura ContentViewdeve ficar assim:

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)
    }
}

E é tudo - aplicamos nosso design neomórfico comum em dois lugares!

Melhoria da acessibilidade


Tivemos tempo suficiente para brincar com vários estilos neomórficos, mas agora quero me concentrar nos problemas desse design: uma extrema falta de contraste significa que botões e outros controles importantes não são suficientes para se destacar do ambiente, o que dificulta o uso de nossos aplicativos para pessoas com visão imperfeita.

Este é o momento em que observo alguns mal-entendidos, por isso quero dizer algumas coisas com antecedência:

  1. Sim, entendo que os botões padrão da Apple se parecem com texto azul e, portanto, não se parecem com botões familiares, pelo menos à primeira vista, mas eles têm uma alta taxa de contraste.
  2. « , , » — — « », , , - .
  3. - SwiftUI, , VoiceOver Apple.

Você já viu como alteramos o ícone cinza para branco para obter aprimoramento instantâneo do contraste, mas os botões e interruptores ainda precisam de muito mais contraste se você quiser torná-los mais acessíveis.

Então, vamos considerar quais mudanças poderíamos fazer para que os elementos realmente se destacassem.

Primeiro, gostaria que você adicionasse duas novas cores à nossa extensão:

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)

Em segundo lugar, duplique o existente DarkBackgrounde nomeie a cópia ColorfulBackground. Nós vamos lidar com isso em um momento, mas, novamente, primeiro precisamos fazer alguma preparação.

Terceiro, duplique o estilo escuro do botão e alterne, renomeie-o para ColorfulButtonStylee ColorfulToggleStyle, em seguida, faça-os usar o novo ColorfulBackgroundcomo plano de fundo.

Então eles devem ficar assim:

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())
        )
    }
}

E, finalmente, edite o botão e alterne ContentViewpara usar o novo 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())

Você pode executar o aplicativo, se quiser, mas isso não faz sentido - ele não mudou.

Para trazer nossa versão colorida da vida, mudaremos os modificadores fill()e overlay()os estados pressionado e não pressionado. Assim, quando isHighlightedverdadeiro, mudar darkStarttanto darkEndpara lightStarte lightEnd, como este:

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)

Se você executar o aplicativo novamente, verá que ele já melhorou significativamente: o estado pressionado agora possui uma cor azul brilhante, tornando-se claro quando os botões são pressionados e os comutadores estão ativos. Mas podemos fazer outra coisa - podemos adicionar a mesma cor ao redor do botão quando ele não é pressionado, ajudando a chamar a atenção para ele.



Para fazer isso, altere o overlay()estado não estressado existente para isso:

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


Portanto, o estilo do botão final deve ficar assim:

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)
    }
}

Agora execute o aplicativo novamente e você verá que um anel azul apareceu ao redor dos botões e interruptores e, quando clicado, é preenchido com azul - é muito mais acessível.



Conclusão


Na prática, você não terá vários estilos de botão diferentes em um projeto, pelo menos se não gostar de criar uma dor de cabeça para seus usuários. Mas este é um espaço interessante para experimentação, e espero que você entenda essa ideia do meu artigo, porque você pode criar efeitos realmente bonitos sem rasgar suas costas.

Eu disse repetidamente que você deve sempre monitorar a disponibilidade do seu aplicativo, e isso significa mais do que apenas garantir que o VoiceOver funcione com a interface do usuário. Certifique-se de que seus botões pareçam interativos, verifique se os rótulos e os ícones de texto têm uma taxa de contraste suficiente em relação ao plano de fundo (pelo menos 4,5: 1, mas tendem a 7: 1) e verifique se as áreas clicáveis ​​são confortáveis ​​e grande (pelo menos 44x44 pixels).

E, pelo amor de Deus, use o design neomórfico para experimentar e expandir seu conhecimento do SwiftUI, mas nunca se esqueça de que, se você sacrifica a usabilidade por uma nova tendência de design de moda, não ganha nada.

Você pode obter o código-fonte completo para este projeto no GitHub .

Leia a primeira parte.



Saiba mais sobre o curso.



All Articles