Néomorphisme utilisant SwiftUI. Partie 1

Salut, Khabrovites! En prévision du lancement du cours avancé «Développeur IOS», nous avons préparé une autre traduction intéressante.




Le design non morphique est peut-être la tendance la plus intéressante de ces derniers mois, bien qu'en réalité Apple l'ait utilisé comme motif de design lors de la WWDC18. Dans cet article, nous verrons comment vous pouvez implémenter une conception non morphique à l'aide de SwiftUI, pourquoi vous voudrez peut-être le faire et - plus important encore - comment nous pouvons affiner cette conception pour augmenter son accessibilité.
Important : le néomorphisme - également parfois appelé neuromorphisme - a de graves conséquences sur l'accessibilité, donc malgré la tentation de lire la première partie de cet article et de sauter le reste, je vous invite à lire l'article jusqu'au bout et à étudier les avantages et les inconvénients afin que vous puissiez voir l'image complète .


Bases du néomorphisme


Avant de passer au code, je voudrais brièvement décrire deux principes de base de cette direction dans la conception, car ils seront pertinents au fur et à mesure:

  1. Le néomorphisme utilise l'éblouissement et l'ombre pour déterminer les formes des objets à l'écran.
  2. Le contraste a tendance à diminuer; complètement blanc ou noir ne sont pas utilisés, ce qui vous permet de mettre en évidence les hautes lumières et les ombres.

Le résultat final est un look qui rappelle le «plastique extrudé» - une conception d'interface utilisateur qui a certainement l'air fraîche et intéressante sans vous heurter les yeux. Je ne peux que répéter encore une fois que la réduction du contraste et l'utilisation des ombres pour mettre en évidence les formes affectent sérieusement l'accessibilité, et nous y reviendrons plus tard.

Cependant, je pense toujours que le temps passé à apprendre sur le néomorphisme dans SwiftUI en vaut la peine - même si vous ne l'utilisez pas dans vos propres applications, c'est comme un kata d'écriture de code pour vous aider à perfectionner vos compétences.

D'accord, assez de bavardages - passons au code.

Construire une carte non morphique


Le point de départ le plus simple est de créer une carte non morphique: un rectangle arrondi qui contiendra des informations. Ensuite, nous verrons comment nous pouvons transférer ces principes à d'autres parties de SwiftUI.

Commençons par créer un nouveau projet iOS à l'aide du modèle d'application Single View. Assurez-vous que vous utilisez SwiftUI pour l'interface utilisateur, puis nommez le projet Neumorphism.

Astuce: si vous avez accès à l'aperçu de SwiftUI dans Xcode, je vous recommande de l'activer immédiatement - il vous sera beaucoup plus facile d'expérimenter.

Nous allons commencer par définir une couleur qui représente une teinte crémeuse. Ce n'est pas du gris pur, mais plutôt une nuance très subtile qui ajoute un peu de chaleur ou de fraîcheur à l'interface. Vous pouvez l'ajouter au répertoire des ressources si vous le souhaitez, mais maintenant c'est plus facile à faire en code.

Ajoutez ceci en Colordehors de la structure ContentView:

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

Oui, il est presque blanc, mais il est assez sombre pour donner au vrai blanc une lueur quand nous en avons besoin.

Maintenant, nous pouvons remplir le corps ContentViewen le fournissant ZStack, qui occupe tout l'écran, en utilisant notre nouvelle couleur quasi-blanche pour remplir tout l'espace:

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

Pour représenter notre carte, nous utiliserons un rectangle arrondi dans la résolution de 300x300 pour la rendre belle et claire à l'écran. Ajoutez donc ceci à ZStack, sous la couleur:

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

Par défaut, il sera noir, mais pour la mise en œuvre du néomorphisme, nous voulons réduire fortement le contraste, nous allons donc le remplacer par la même couleur que nous utilisons pour l'arrière-plan, ce qui rend en fait la forme invisible.

Alors changez-le comme ceci:

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

Un point important: nous déterminons la forme en utilisant des ombres, une sombre et une claire, comme si la lumière projetait des rayons depuis le coin supérieur gauche de l'écran.

SwiftUI nous permet d'appliquer plusieurs fois des modificateurs, ce qui facilite l'implémentation du néomorphisme. Ajoutez les deux modificateurs suivants à votre rectangle arrondi:

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

Ils représentent le décalage de l'ombre sombre dans le coin inférieur droit et le décalage de l'ombre claire dans le coin supérieur gauche. L'ombre claire est visible car nous avons utilisé un fond quasi blanc, et maintenant la carte devient visible.

Nous n'avons écrit que quelques lignes de code, mais nous avons déjà une carte non morphique - j'espère que vous conviendrez que SwiftUI facilite étonnamment le processus!



Création d'un simple bouton non morphique


De tous les éléments d'une interface utilisateur, le néomorphisme présente un risque assez faible pour les cartes - si l'interface utilisateur à l'intérieur de vos cartes est claire, la carte peut ne pas avoir de bordure claire, et cela n'affectera pas l'accessibilité. Les boutons sont une autre affaire, car ils sont conçus pour interagir, donc réduire leur contraste peut faire plus de mal que de bien.

Traitons cela en créant notre propre style de bouton, car c'est ainsi que SwiftUI permet aux configurations de bouton d'être distribuées à de nombreux endroits. C'est beaucoup plus pratique que d'ajouter de nombreux modificateurs à chaque bouton que vous créez - nous pouvons simplement définir le style une fois et l'utiliser à de nombreux endroits.

Nous allons définir un style de bouton qui sera en fait vide: SwiftUI nous donnera le libellé du bouton, qui peut être du texte, une image ou autre chose, et nous le renverrons sans modifications.

Ajoutez cette structure quelque part à l'extérieur ContentView:

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

C'est configuration.labelcelui-ci qui contient le contenu du bouton, et bientôt nous ajouterons autre chose. Définissons d'abord un bouton qui l'utilise pour que vous puissiez voir comment le design évolue:

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

Vous ne verrez rien de spécial à l'écran, mais nous pouvons y remédier en ajoutant notre effet non morphique au style du bouton. Cette fois, nous n'utiliserons pas de rectangle arrondi, car pour les icônes simples, le cercle est meilleur, mais nous devons ajouter une indentation pour que la zone de clic du bouton soit grande et belle.
Modifiez votre méthode makeBody()en ajoutant une indentation, puis en plaçant notre effet non morphique comme arrière-plan du bouton:

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



Cela nous rapproche suffisamment de l'effet souhaité, mais si vous exécutez l'application, vous verrez qu'en pratique, le comportement n'est toujours pas parfait - le bouton ne réagit pas visuellement lorsqu'il est enfoncé, ce qui semble juste bizarre.

Pour résoudre ce problème, nous devons lire la propriété configuration.isPresseddans notre style de bouton personnalisé, qui indique si le bouton est actuellement enfoncé ou non. Nous pouvons l'utiliser pour améliorer notre style et donner une indication visuelle de la pression du bouton.

Commençons par un simple: nous utiliserons des Groupboutons pour l'arrière-plan, puis vérifierons configuration.isPressedet retournerons soit un cercle plat si le bouton est enfoncé, soit notre cercle sombre actuel sinon:

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

Puisqu'un isPressedcercle avec une couleur quasi-blanche est utilisé dans l'état , il rend notre effet invisible lorsque le bouton est enfoncé.

Avertissement: en raison de la façon dont SwiftUI calcule les zones enregistrables, nous avons involontairement rendu la zone de clic pour notre bouton très petite - vous devez maintenant appuyer sur l'image elle-même, et non sur la conception non morphologique qui l'entoure. Pour résoudre ce problème, ajoutez un modificateur .contentShape(Circle())immédiatement après .padding(30), forçant SwiftUI à utiliser tout l'espace disponible du robinet.

Maintenant, nous pouvons créer l'effet de la concavité artificielle en inversant l'ombre - en copiant deux modificateurs shadowde l'effet de base, en échangeant les valeurs X et Y pour le blanc et le noir, comme indiqué ici:

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

Estimez le résultat.



Créer des ombres internes pour un clic de bouton


Notre code actuel, en principe, fonctionne déjà, mais les gens interprètent l'effet différemment - certains le voient comme un bouton concave, d'autres voient que le bouton n'est toujours pas enfoncé, juste la lumière vient d'un angle différent.

L'idée de l'amélioration est de créer une ombre intérieure qui simulera l'effet d'appuyer sur le bouton vers l'intérieur. Cela ne fait pas partie du kit SwiftUI standard, mais nous pouvons l'implémenter assez facilement.

La création d'une ombre intérieure nécessite deux dégradés linéaires, et ils ne seront que le premier des nombreux dégradés internes que nous utiliserons dans cet article, nous allons donc ajouter immédiatement une petite extension d'assistance pour LinearGradientsimplifier la création de dégradés standard:

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

Avec cela, nous pouvons simplement fournir une liste variable de couleurs pour récupérer leur dégradé linéaire dans la direction diagonale.

Maintenant, sur le point important: au lieu d'ajouter deux ombres inversées à notre cercle pressé, nous allons superposer un nouveau cercle avec un flou (trait), puis appliquer un autre cercle avec un dégradé comme masque. C'est un peu plus compliqué, mais permettez-moi de l'expliquer au coup par coup:

  • Notre cercle de base est notre cercle actuel avec un effet néomorphique, rempli d'une couleur quasi blanche.
  • Nous plaçons un cercle dessus, encadré par un cadre gris, et floutons légèrement pour adoucir ses bords.
  • Ensuite, nous appliquons un masque avec un autre cercle à ce cercle superposé en haut, cette fois rempli d'un dégradé linéaire.

Lorsque vous appliquez une vue en tant que masque pour une autre, SwiftUI utilise le canal alpha du masque pour déterminer ce qui doit être affiché dans la vue de base.

Donc, si nous dessinons un trait gris flou, puis le masquons avec un dégradé linéaire du noir au transparent, le trait flou sera invisible d'un côté et augmentera progressivement de l'autre - nous obtiendrons un dégradé interne lisse. Pour rendre l'effet plus prononcé, nous pouvons légèrement déplacer les cercles ombrés dans les deux sens. Après avoir expérimenté un peu, j'ai trouvé que dessiner une ombre claire avec une ligne plus épaisse qu'une sombre aide à maximiser l'effet.

N'oubliez pas que deux ombres sont utilisées pour créer un sentiment de profondeur dans le néomorphisme: une claire et une sombre, nous ajouterons donc cet effet de l'ombre intérieure deux fois avec des couleurs différentes.

Modifiez le cercle configuration.isPresscomme suit:

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 vous exécutez à nouveau l'application, vous verrez que l'effet d'appuyer sur un bouton est beaucoup plus prononcé et semble meilleur.



Sur ce point, la première partie de la traduction a pris fin. Dans les prochains jours nous publierons la suite, et maintenant nous vous invitons à en savoir plus sur le cours à venir .

All Articles