Неоморфизм с помощью SwiftUI. Часть 2: Что можно сделать с доступностью?

    Всем привет! В преддверии старта продвинутого курса «Разработчик IOS» мы публикуем перевод второй части статьи про неоморфизм с помощью SwiftUI (читать первую часть).





    Темная тема


    Прежде чем мы начнем разбираться с тем, как мы можем улучшить доступность неоморфизма, давайте посмотрим, как мы можем поработать с эффектом, чтобы создать другие интересные стили.

    Сначала добавьте еще два цвета в расширение Color, чтобы у нас под рукой было несколько константных значений:

    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)

    Мы можем использовать их в качестве фона для ContentView, заменив существующий Color.white, как показано здесь:

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

    Наш SimpleButtonStyle теперь выглядит неуместно, потому что он накладывает яркую стилизацию на темный фон. Итак, мы собираемся создать новый темный стиль, который подойдет сюда лучше, но на этот раз мы разделим его на две части: фоновое представление, которое мы можем использовать где угодно, и стиль кнопки, который оборачивает ее с модификаторами padding и contentShape. Это даст нам больше гибкости, как вы потом увидите.

    Новый view фона, который мы собираемся добавить, позволит нам указать любую форму для нашего визуального эффекта, поэтому мы больше не привязаны к окружностям. Он также будет отслеживать, следует ли отрисовывать вогнутым наш выпуклый эффект (внутрь или наружу) в зависимости от свойства isHighlighted, которое мы можем менять извне.

    Начнем мы с самого простого, используя модифицированный подход флипа теней для получения эффекта вогнутости. Добавьте эту структуру:

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

    Модификация заключается в том, что при нажатии кнопки размер тени уменьшается — мы используем расстояние в 5 точек вместо 10.

    Дальше мы можем обернуть это с помощью DarkButtonStyle, который применяется padding и contentShape, как показано здесь:

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

    И наконец, мы можем применить это для нашей кнопки в ContentView путем изменения ее ButtonStyle():

    .buttonStyle(DarkButtonStyle())

    Посмотрим, что получилось — хотя у нас не так много кода, я думаю, что результат выглядит достаточно хорошо.



    Немного экспериментов


    Теперь самое время поэкспериментировать с эффектом, потому что это поможет вам лучше понять, на что конкретно способен SwiftUI.

    Например, мы могли бы создать плавную выпуклую кнопку, добавив к ней линейный градиент и флипать его при нажатии:

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

    Если вы запустите это, вы увидите, что кнопка плавно анимируется вверх и вниз при нажатии и отпускании. Я считаю, что анимация немного отвлекает, поэтому я рекомендую отключить ее, добавив этот модификатор в метод makeBody() из DarkButtonStyle, после присутствующего там модификатора background():

    .animation(nil)



    Этот эффект кнопки-“подушки” очарователен, но если вы планируете использовать его, я бы посоветовал вам попробовать три следующих изменения, чтобы кнопка выделялась чуть-чуть побольше.

    Во-первых, несмотря на то, что это противоречит низкоконтрастному принципу неоморфного дизайна, я бы заменил серую иконку на белую, чтобы она выделялась. Итак, в ContentView вам бы понадобилось следующее:

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

    Во-вторых, если вы добавите overlay для кнопки в нажатом состоянии, это не только сделает ее больше похожей на реальную физическую кнопку, нажимаемую равномерно, но и поможет отличить ее нажатое состояние от ненажатого.

    Чтобы реализовать это, вам нужно вставить модификатор overlay() после fill(), когда isHighlighted имеет значение true, как здесь:

    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)



    Для достижения еще более четкого вида вы можете удалить два модификатора shadow() для нажатого состояния, которые фокусируют на overlay в противном случае.

    В-третьих, вы также можете добавить overlay к ненажатому состоянию, просто чтобы отметить, что это кнопка. Поместите его непосредственно после fill(), например, так:

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



    Добавление стиля переключателя


    Одним из преимуществ отделения стиля кнопоки от стиля неоморфного фона является то, что теперь мы можем добавить стиль переключателя с помощью того же эффекта. Это означает создание новой структуры, соответствующей протоколу ToggleStyle, которая похожа на ButtonStyle, за исключением того, что:

    1. Мы должны считывать configuration.isOn, чтобы определить, включен ли переключатель.
    2. Нам нужно предоставить кнопку для обработки акта переключения или, по крайней мере, что-то типа onTapGesture() или в этом духе.

    Добавьте эту структуру в ваш проект:

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

    Мы хотим поместить один из них в ContentView, чтобы вы могли потестировать его сами, поэтому начните с добавления этого свойства:

    @State private var isToggled = false

    Затем оберните существующую кнопку в VStack со spacing равным 40 и поместите ее ниже:

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

    Ваша структура ContentView должна выглядеть следующим образом:

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

    И это все — мы применили наш общий неоморфный дизайн в двух местах!

    Улучшение доступности


    У нас было достаточно времени, чтобы поиграть с различными неоморфными стилями, но теперь я хочу остановиться на проблемах этого дизайна: чрезвычайный недостаток контраста означает, что кнопки и другие важные элементы управления недостаточно выделяются на фоне их окружения, что затрудняет использование наших приложений для людей с неидеальным зрением.

    Это тот момент, вокруг которого я наблюдаю некоторое недопонимание, поэтому я хочу проговорить несколько вещей заранее:

    1. Да, я понимаю, что стандартные кнопки Apple выглядят просто как синий текст и поэтому не напоминают привычные кнопки, по крайней мере, на первый взгляд, но они имеют высокий коэффициент контрастности.
    2. Недостаточно сказать «мне и так нравится, но я могу добавить специальную опцию, чтобы сделать это более доступным» — доступность — это не «приятный бонус», это требование, поэтому наши приложения должны быть доступны по умолчанию, а не путем выполнения каких-либо процедур.
    3. Созданные нами элементы управления по-прежнему являются кнопками и переключателями SwiftUI, что означает, что все наши изменения не повлияют на их видимость или функциональность для VoiceOver или других вспомогательных технологий Apple.

    Вы уже видели, как мы меняли серую иконку на белую, чтобы получить мгновенное усиление контраста, но кнопки и переключатели все еще нуждаются в гораздо большей контрастности, если вы хотите сделать их доступнее.

    Итак, мы собираемся рассмотреть, какие изменения мы могли бы внести, чтобы элементы действительно выделялись.

    Во-первых, я хотел бы, чтобы вы добавили два новых цвета в наше расширение:

    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)

    Во-вторых, продублируйте существующий DarkBackground и назовите копию ColorfulBackground. Мы займемся им через мгновение, но опять-таки сначала нам нужно провести некоторую подготовку.

    В-третьих, продублируйте темный стиль кнопки и переключателя, переименуйте их в ColorfulButtonStyle и ColorfulToggleStyle, а затем заставьте их использовать новый ColorfulBackground в качестве фона.

    Итак, они должны выглядеть следующим образом:

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

    И, наконец, отредактируйте кнопку и переключатель в ContentView, чтобы они использовали новый стиль:

    Button(action: {
        print("Button tapped")
    }) {
        Image(systemName: "heart.fill")
            .foregroundColor(.white)
    }
    .buttonStyle(ColorfulButtonStyle())
    
    Toggle(isOn: $isToggled) {
        Image(systemName: "heart.fill")
            .foregroundColor(.white)
    }
    .toggleStyle(ColorfulToggleStyle())

    Вы можете запустить приложение, если хотите, но в этом нет особого смысла — оно фактически не изменилось.

    Чтобы воплотить нашу красочную версию в жизнь, мы собираемся изменить модификаторы fill() и overlay() для нажатых и ненажатых состояний. Таким образом, когда isHighlighted имеет значение true, измените darkStart и darkEnd на lightStart и lightEnd, вот так:

    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)

    Если вы снова запустите приложение, вы увидите, что оно уже значительно улучшилось: нажатое состояние теперь имеет ярко-синий цвет, поэтому становится ясно, когда кнопки нажаты, а переключатели активны. Но мы можем сделать еще кое-что — мы можем добавить тот же цвет вокруг кнопки, когда она не нажата, помогая привлечь к ней внимание.



    Чтобы сделать это, изменить существующий overlay() для ненажатого состояния на это:

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


    Итак, готовый стиль кнопки должен выглядеть следующим образом:

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

    Теперь снова запустите приложение, и вы увидите, что вокруг кнопок и переключателей появилось синее кольцо, и при нажатии оно заполняется синим цветом — это намного доступнее.



    Заключение


    На практике у вас не будет нескольких разных стилей кнопок в одном проекте, по крайней мере, если вы не любите создавать головную боль для своих пользователей. Но это интересное пространство для экспериментов, и я надеюсь что вы уловили эту идею моей статьи, потому что вы можете создавать действительно красивые эффекты не надрывая спину.

    Я неоднократно говорил, что вы всегда должны следить за доступностью своего приложения, и это означает больше, чем просто убедиться, что VoiceOver работает с вашим пользовательским интерфейсом. Убедитесь, что ваши кнопки выглядят интерактивно, убедитесь, что ваши текстовые лейблы и иконки имеют достаточный коэффициент контрастности по отношению к фону (не менее 4,5:1, но стремятся к 7:1), и убедитесь, что ваши кликабельные области удобные и большие (не менее 44x44 точек).

    И ради всего святого используйте неоморфный дизайн для экспериментов и расширения своего знания SwiftUI, но никогда не забывайте, что если вы жертвуете юзабилити ради нового модного дизайнерского тренда, вы ничего не выигрываете.

    Получить полный исходный код для этого проекта можно на GitHubю

    Читать первую часть.



    Узнать подробнее о курсе.


    OTUS. Онлайн-образование
    Цифровые навыки от ведущих экспертов

    Похожие публикации

    Комментарии 0

    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

    Самое читаемое