Неоморфизм с помощью SwiftUI. Часть 1

Автор оригинала: Paul Hudson
  • Перевод
Салют, хабровчане! В преддверии старта продвинутого курса «Разработчик IOS» мы подготовили еще один интересный перевод.




Неоморфный дизайн — это, пожалуй, самый интересный тренд последних месяцев, хотя, по правде говоря, Apple использовала его в качестве своего дизайнерского мотива еще на WWDC18. В этой статье мы рассмотрим, как вы можете реализовать неоморфный дизайн с помощью SwiftUI, почему вы могли бы захотеть это сделать, и — что наиболее важно — как мы можем доработать этот дизайн, чтобы повысить его доступность.
Важно: Неоморфизм — также иногда называемый нейморфизмом — имеет серьезные последствия для доступности, поэтому, несмотря на соблазн прочитать первую часть этой статьи и пропустить остальное, я призываю вас дочитать статью до конца и изучить как достоинства, так и недостатки, чтобы вы увидели полную картину.


Основы неоморфизма


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

  1. Неоморфизм использует блики и тени для определения форм объектов на экране.
  2. Контраст, как правило, уменьшается; полностью белый или черный не используются, что позволяет выделять блики и тени.

Конечным результатом является вид, напоминающий «экструдированный пластик» — дизайн пользовательского интерфейса, который, безусловно, выглядит свежо и интересно, не врезаясь в ваши глаза. Я не могу не повторить еще раз, что уменьшение контраста и использование теней для выделения фигур серьезно влияет на доступность, и мы вернемся к этому позже.

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

Хорошо, достаточно трепа — давайте перейдем к коду.

Построение неоморфной карты


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

Начнем с создания нового проекта iOS с использованием шаблона приложения Single View. Убедитесь, что вы используете SwiftUI для пользовательского интерфейса, а затем назовите проект Neumorphism.

Совет: если у вас есть доступ к предварительному просмотру SwiftUI в Xcode, я рекомендую вам активировать его сразу же — вам будет гораздо проще экспериментировать.

Мы начнем с определения цвета, представляющего кремовый оттенок. Это не чистый серый, а скорее очень тонкий оттенок, который добавляет немного тепла или прохлады в интерфейс. Вы можете добавить его в каталог ассетов, если хотите, но сейчас это проще сделать в коде.

Добавьте этот Color вне структуры ContentView:

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

Да, это практически белый цвет, но он достаточно темный для того, чтобы настоящий белый воспринимался как свечение, когда нам это нужно.

Теперь мы можем заполнить body ContentView, предоставив ему ZStack, который занимает весь экран, используя наш новый квазибелый цвет для заполнения всего пространства:

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

Для представления нашей карты мы будем использовать прямоугольник со скругленными углами в разрешении 300x300, чтобы сделать его красивым и четким на экране. Итак, добавьте это в ZStack, под цвет:

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

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

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

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

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

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

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

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

Мы написали всего несколько строк кода, но у нас уже есть неоморфная карта — надеюсь, вы согласитесь, что SwiftUI удивительно облегчает процесс!



Создание простой неоморфной кнопки


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

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

Мы собираемся определить стиль кнопки, который фактически будет пустым: SwiftUI передаст нам label для кнопки, которая может быть текстом, изображением или чем-то еще, и мы отправим его обратно без изменений.

Добавьте эту структуру где-то за пределами ContentView:

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

Этот configuration.label — это то, что вмещает содержимое кнопки, и вскоре мы будем добавлять туда кое-что еще. Во-первых, давайте определим кнопку, которая его использует, чтобы вы могли увидеть, как дизайн развивается:

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

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

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



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

Чтобы это исправить, нам нужно считывать свойство configuration.isPressed внутри нашего пользовательского стиля кнопки, который сообщает, нажата ли кнопка в данный момент или нет. Мы можем использовать это для улучшения нашего стиля, чтобы дать некоторую визуальную индикацию того, нажата ли кнопка.

Давайте начнем с простого: мы будем использовать Group для фона кнопки, затем проверять configuration.isPressed и возвращать либо плоский круг, если кнопка нажата, либо наш текущий затемненный круг в противном случае:

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

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

Предупреждение: из-за способа, которым SwiftUI вычисляет тапабельные области, мы ненарочно сделали площадь нажатия для нашей кнопки очень маленькой — сейчас вам нужно тапать на само изображение, а не на неоморфный дизайн вокруг него. Чтобы исправить это, добавьте модификатор .contentShape(Circle()) сразу после .padding(30), заставляя SwiftUI использовать все доступное пространство для тапа.

Теперь мы можем создать эффект искусственной вогнутости флипнув тени — путем копирования двух модификаторов shadow из базового эффекта, обменяв значения X и Y для белой и черной, как показано здесь:

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

Оценим результат.



Создание внутренних теней для нажатия кнопки


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

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

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

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

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

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

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

Когда вы применяете одно представление в качестве маски для другого, SwiftUI использует альфа-канал маски, чтобы определить, что должно отображаться в базовом представлении.

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

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

Измените окружность configuration.isPress следующим образом:

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

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



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

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

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

    0
    Я надеюсь неоморфизм так же скоро уйдет в небытие, как и появился
      0
      А что с ним не так?
        0
        Он не подходит для людей с проблемами зрения, он создает путаницу и заставляет лишний раз пользователя задуматься — что включено, отмечено, где кликабельно а где нет

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

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