Apprentissage de SwiftUI

Bonjour tout le monde.

J’ai récemment découvert SwiftUI grâce au cours de @mbritto, et je suis tombé sous le charme (de SwiftUI, pas de Maxime, même s’il est très sympathique :joy:). C’est plus naturel pour moi qu’UIKit, avec lequel j’ai plus de mal.

N’ayant aucune idée d’application à développer (:frowning:), je me suis mis comme défi de refaire l’interface de l’application Instagram, mais uniquement en SwiftUI.

Je n’ai fait que l’écran principal pour l’instant, avec des données statiques (le but est uniquement de se concentrer sur l’aspect UI). Je n’ai pas encore implémenté d’interaction quelconque (aimer une photo par exemple).

J’ai partagé mon projet sur GitHub.

J’ai quelques questions :

  1. Je souhaite que la zone « stories » et surtout les photos débordent sur la safe area. Pour cela, j’ai essayé d’ajouter le modificateur edgesIgnoringSafeArea(.all) un peu partout, mais rien à faire, ça ne change rien :frowning:

  2. un « post » contient différentes informations, dont notamment un auteur et une localisation optionnelle. J’affiche la photo de profil de l’auteur, à côté son nom et, le cas échéant, la localisation en dessous. Pour que le bloc nom + localisation soit aligné verticalement avec la photo de profil, je teste la présence de la localisation à l’aide d’une variable calculée (private var hasLocation: Bool { if let _ = post.location { return true } else { return false } }) puis affiche uniquement une vue Text() si besoin (if hasLocation { Text(post.location!) }). Est-ce la bonne façon de faire ? N’est-il pas possible de simplifier cela ?

  3. Enfin (mais je ne me suis pas penché plus que ça sur le sujet), quand j’exécute l’application sur iPad (que ce soit dans le simulateur ou sur mon appareil), elle garde les mêmes dimensions que sur iPhone. Il me semblait que SwiftUI arrivait justement à s’adapter aux tailles d’écran des différents systèmes, non ?

Merci d’avance de votre aide !

2 « J'aime »

Hello,

Pour ta premiere question, il faut que tu ajoutes le modifier « .listRowInsets ».
Comme ceci

Capture d’écran 2020-07-01 à 17.28.09

Ce qui donne

Tu peux regarder ce petit tuto fourni par Apple il contient pas mal de petit truc comme ca.
SwiftUI: composing complex interfaces

Pour l’affichage sur iPad je ne suis pas certains mais je pense que c’est du à la navigationView. Elle n’est pas géré de la meme façon sur iPad.
De toute façon Apple avec Swift2 nous annonce que la tabBar sur iPad doit maintenant être une sideBar.
A creuser de ce coté je pense.

En tout cas top ton petit projet :+1:t3:

Seb.

2 « J'aime »

Bon choix, sOta, le dragon est bien, même s’il est trop rouge.

Un peu de lecture sur le sujet : https://www.swiftbysundell.com/tips/optional-swiftui-views/

EDIT : Voici une version simplifiée de ta Vue Auteur, utilisant l’une des techniques décrites dans le lien :

struct Author: View {
        var post: Post
        
        var body: some View {
            HStack(alignment: .center) {
                Image(post.author.photo).clipShape(Circle())
                VStack(alignment: .leading) {
                    Text(post.author.name).font(.subheadline).fontWeight(.semibold).onTapGesture {
                        print("Tapped \(self.post.author)")
                    }
                  post.location.map { location in
                    Text(location).font(.caption) }
                }
                Spacer()
                Image(systemName: "ellipsis").onTapGesture {
                    print("Tapped ellipsis")
                }
            }
        }
    }

J’ai testé avec ton code, ca semble fonctionner parfaitement.

EDIT 2 :

On peut aussi utiliser une approche brutale, en testant la valeur nil.

          if post.location != nil {
            Text(post.location!).font(.caption)
          }

C’est rustique, loin de la sophistication habituelle de Swift, mais efficace !

1 « J'aime »

Super, un grand merci !

Effectivement ça fonctionne avec .listRowInsets.

Ensuite il faut ajouter des .padding sur les autres éléments d’un post pour les décoller de la safe area. C’est un peu fastidieux et j’y suis allé à tâtons, mais au final le rendu est celui que je souhaite :slight_smile:

Je vais compulser ton lien, ainsi que les autres étapes du tutoriel d’Apple. Merci.

1 « J'aime »

Je suis fan de jeux de société (plutôt cubes en bois que figurines en plastique), alors je devais bien placer une photo en rapport dans « mon » Instagram :slight_smile: C’est la première sur laquelle je suis tombée sur le site d’images libres de droits.

C’est bien plus propre avec .map ! Je pensais que ça ne s’appliquait qu’à un Array.

Merci !

Bah non, il te suffit de mettre tous ces éléments dans une Stack et d’appliquer le .padding une seule fois.


Je pensais la même chose, j’ai été étonné de voir ce code dans le tutoriel sur la manière de simuler un if let dans une View SwiftUI. Mais c’est bien pratique.

Dans la foulée, j’ai essayé un if let dans une View avec SwiftUI 2.0 (avec la Beta d’Xcode 12). Cela ne fonctionne toujours pas. Dommage …

Je persiste à penser qu’un honnête dragon est bleu ou métallique !

@MonsieurSaucisse félicitations pour ton app UI !

Par contre, je trouve que la structure de ton code est à revoir. Tu as tout mis dans un seul fichier, alors que tu pourrais séparer tes éléments graphiques.

Ça te permettrait aussi de debugger plus facilement, en cliquant sur les éléments graphiques pour sélectionner le code ou vis versa.

1 « J'aime »

Je m’étais fait la réflexion aussi… ton commentaire m’a motivé pour le faire :slight_smile:

J’ai donc séparé en plusieurs fichiers, mais j’avoue avoir du mal à trouver comment les structurer, choisir un nom adéquat, etc.

J’en ai aussi profité pour utiliser deux extensions, pour Color et Image, ça rend le code nettement plus léger. Et encore, je n’ai pas fait ça très proprement :grimacing:

J’ai trouvé comment ajouter une bordure en dégradé autour des stories, j’ai étendu les stories et les photos sur toute la largeur grâce à @s0ta, j’ai ajouté la colorisation des icônes cœur et marque-page, et j’ai ajouté une actionSheet sur l’ellipse à droite de l’auteur de la photo… je suis assez content du résultat.

Définitivement, j’aime beaucoup SwiftUI :heart_eyes:

1 « J'aime »

Pourtant c’est à priori permis dans Xcode 12 :

struct ContentView: View {
    @State var username: String?

    var body: some View {
        Group {
            if let username = username {
                Text("Welcome, \(username)")
            } else {
                Image(systemName: "questionmark.circle")
            }
        }
        .font(.largeTitle)
    }
}

Oups, tu as raison. J’avais tapé un petit test en vitesse, a 2h00 du matin, avec une grosse erreur dans le code (tellement ridicule que je n’ose pas la reproduire ici).

J’ai corrigé et ça fonctionne maintenant. Merci.

Merci @ThonyF pour la pull request ! :slight_smile:

C’est effectivement plus lisible comme ça.

1 « J'aime »

@MonsieurSaucisse Service !

J’ai corrigé le bug de l’iPad. Ça vient de la NavigationView qui est, en SwiftUI, par défaut, comme sur l’app Mail d’Apple.
La première vue de la NavigationView n’est pas affichée automatiquement, c’est la seconde vue qui l’est et quand tu cliques sur le bouton de navigation, c’est un bandeau qui apparaît.

D’ailleurs, ce comportement est aussi valable sur les iPhone en mode paysage.

Donc, j’ai modifier le style par défaut du NavigationView en ajoutant cette ligne : .navigationViewStyle(StackNavigationViewStyle())

Et je ne sais pas, si tu as remarqué, mais dans ta barre des stories, il n’y a pas le même espacement entre les photos de profils, qui n’y est pas dans l’App officielle.

2 « J'aime »

Merci pour l’astuce avec StackNavigationViewStyle !

Pour information, tu avais ajouté le mode sombre sur la prévisualisation avec .colorScheme(.dark), qui est déprécié. Il faut utiliser preferredColorScheme(.dark) à la place (et il n’est plus nécessaire de spécifier le fond en noir).

J’ai un peu revu l’architecture et le code, et le résultat me convient. J’ai notamment remplacé List par ScrollView. Les fonctionnalités apportées par List n’étaient pas utiles dans ce cas.

Sauf deux détails qui me posent problème encore :

  1. Comme souligné par @ThonyF, l’espacement entre les photos de profil des stories n’est pas toujours le même. Je ne sais pas comment corriger ça :frowning:

  2. En zoomant, on voit des imperfections graphiques sur la première photo de profil des stories (cf. ci-dessous), autour de la photo et du « + ». J’ai essayé en utilisant ZStack ou .overlay(), le résultat est le même. Je ne sais pas d’où ça vient ni comment m’en débarrasser.

Voici le code concerné :

Image("profile00")
    .resizable()
    .clipShape(Circle())
    .frame(width: 65, height: 65)
    .overlay(Circle()
        .strokeBorder(Color.systemBackground, lineWidth: 5))
Image(systemName: "plus.circle.fill")
    .resizable()
    .foregroundColor(.blue)
    .background(Color.systemBackground)
    .clipShape(Circle())
    .frame(width: 17, height: 17)
    .overlay(Circle()
        .strokeBorder(Color.systemBackground, lineWidth: 2))

Pas facile à reproduire, sans connaître Color.systemBackground.

Le problème n’existant pas avec la version que tu as mise en ligne, je présume que cela viens de ta couleur de fond.

Cela ressemble beaucoup à un problème d’anti-aliasing, entre deux couleurs proches mais différentes quand même.

Il faut plus de code pour reproduire le problème et voir comment y remédier.

Oui pardon, Color.systemBackground est en fait Color(UIColor.systemBackground). Je n’ai pas trouvé comment faire autrement pour prendre la couleur par défaut du fond.

Finalement, j’ai eu une idée juste après avoir envoyé le message… qui fonctionne :slight_smile:

J’ai utilisé plusieurs ZStack imbriqués :

ZStack(alignment: .bottomTrailing) {
    ZStack {
        Circle().fill(Color.systemBackground)
            .frame(width: 65, height: 65)
        Image("profile00")
            .resizable()
            .clipShape(Circle())
            .frame(width: 55, height: 55)
    }
    ZStack {
        Circle().fill(Color.systemBackground)
            .frame(width: 19, height: 19)
        Image(systemName: "plus.circle.fill")
            .resizable()
            .foregroundColor(.blue)
            .background(Color.systemBackground)
            .clipShape(Circle())
            .frame(width: 17, height: 17)
        }
    }

Tout le code est sur mon dépôt GitHub.

Merci pour l’info, je ne l’ai pas fait sous la version bêta de Xcode 12 mais, sous Xcode 11 donc, je n’ai pas remarqué de problème.

Indice : Ce n’est pas la photo le problème, mais le texte :wink:

Trouvé !

Un cercle est composé d’un contour d’une épaisseur de 1 point, et du remplissage de l’intérieur.

Tu dessines une image de 65x65 pixels, puis tu y superposes un cercle de même taille, avec une épaisseur de 5 points. Le problème c’est l’utilisation de l’opérateur .strokeBorder() qui n’agit que sur le remplissage, donc sur un cercle de 64x64 pixels ! Le contour de l’image n’est pas modifiée, ce qui laisse sur l’écran une aura visible.

Pour régler cela, il ne faut pas utiliser .strokeBorder(), mais .stroke() qui lui dessine sur l’écran en partant du contour et non du bord intérieur.

Une reprise de ton code initial, en utilisant .stroke() :

var couleurFond1 = Color.init(UIColor.systemBackground)

struct Version0 : View {
  var body: some View {
    ZStack(alignment: .bottomTrailing) {
      Image("chat")
        .resizable()
        .clipShape(Circle())
        .frame(width: 65, height: 65)
        .overlay(Circle()
          .stroke(couleurFond1, lineWidth: 5))
      Image(systemName: "plus.circle.fill")
        .resizable()
        .foregroundColor(.blue)
        .background(couleurFond1)
        .clipShape(Circle())
        .frame(width: 17, height: 17)
        .overlay(Circle()
          .stroke(couleurFond1, lineWidth: 2))
    }
  }
}

Pas d’effet d’aura sur le résultat :


1 « J'aime »