MFormations
Modern Mobile Engineering

Chapitre 7

07 - iOS UI

07 - iOS UI

07 - iOS UI : Cours complet

Table des matières

  1. UIKit avancé
  2. UICollectionView & UITableView
  3. Navigation controllers, tab bars, modals, popovers
  4. SwiftUI : fondations
  5. State & data flow
  6. @Observable et @Bindable
  7. NavigationStack
  8. Animations & previews
  9. Modifiers
  10. Auto Layout : constraints, stacks, priorities
  11. Diffable data sources
  12. Résumé et checklist

1. UIKit avancé

UIKit reste la base de l'UI iOS et la solution pour les composants complexes. SwiftUI peut l'embarquer via UIViewRepresentable / UIViewControllerRepresentable.

struct WebView: UIViewRepresentable {
    let url: URL

    func makeUIView(context: Context) -> WKWebView {
        WKWebView()
    }

    func updateUIView(_ webView: WKWebView, context: Context) {
        webView.load(URLRequest(url: url))
    }
}

Les patterns UIKit (delegates, data sources) restent pertinents à connaître pour la maintenance et l'interop.


2. UICollectionView & UITableView

2.1 UITableView

Table simple, cellules de liste. Depuis iOS 14, UITableViewDiffableDataSource.

final class FeedViewController: UIViewController {
    private var tableView: UITableView!
    private var dataSource: UITableViewDiffableDataSource<Section, Product>!
    private var products: [Product] = []

    enum Section { case main }

    override func viewDidLoad() {
        super.viewDidLoad()
        setupTable()
        apply(products)
    }

    private func setupTable() {
        tableView = UITableView(frame: .zero, style: .insetGrouped)
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
        view.addSubview(tableView)

        dataSource = UITableViewDiffableDataSource<Section, Product>(tableView: tableView) { tv, indexPath, product in
            let cell = tv.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
            var config = cell.defaultContentConfiguration()
            config.text = product.name
            cell.contentConfiguration = config
            return cell
        }
    }

    private func apply(_ items: [Product]) {
        var snapshot = NSDiffableDataSourceSnapshot<Section, Product>()
        snapshot.appendSections([.main])
        snapshot.appendItems(items)
        dataSource.apply(snapshot, animatingDifferences: true)
    }
}

2.2 UICollectionView

Grille/collection avec layouts flexibles : UICollectionViewCompositionalLayout.

let layout = UICollectionViewCompositionalLayout { sectionIndex, _ in
    // Groupes, items, orthogonal scrolling…
    let item = NSCollectionLayoutItem(
        layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.5), heightDimension: .fractionalHeight(1))
    )
    let group = NSCollectionLayoutGroup.horizontal(
        layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .absolute(180)),
        subitems: [item]
    )
    return NSCollectionLayoutSection(group: group)
}

2.3 Diffable data sources

enum Section {
    case featured, regular
}

struct Item: Hashable {
    let id = UUID()
    let title: String
}

final class CollectionViewController: UIViewController {
    private var collectionView: UICollectionView!
    private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!

    override func viewDidLoad() {
        super.viewDidLoad()
        dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) { cv, indexPath, item in
            let cell = cv.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath)
            var config = cell.defaultContentConfiguration()
            config.text = item.title
            cell.contentConfiguration = config
            return cell
        }
        var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
        snapshot.appendSections([.featured, .regular])
        snapshot.appendItems(featuredItems, toSection: .featured)
        snapshot.appendItems(regularItems, toSection: .regular)
        dataSource.apply(snapshot)
    }
}

Avantages : auto-animation des différences, plus de reloadData() et de indexPath manuels.


3. Navigation controllers, tab bars, modals, popovers

3.1 UINavigationController

Pile de contrôleurs avec back automatique.

let nav = UINavigationController(rootViewController: HomeViewController())
// push
navigationController?.pushViewController(detail, animated: true)
// pop
navigationController?.popViewController(animated: true)

3.2 UITabBarController

Onglets racine.

let tab = UITabBarController()
tab.viewControllers = [
    UINavigationController(rootViewController: HomeVC()),
    UINavigationController(rootViewController: SearchVC()),
    UINavigationController(rootViewController: ProfileVC()),
]

3.3 Modals

let editor = EditorViewController()
editor.modalPresentationStyle = .pageSheet    // ou .formSheet
present(editor, animated: true)

iOS 15+ : .pageSheet par défaut, dismissible par glissement.

3.4 Popovers (iPad)

popover.popoverPresentationController?.sourceView = button
popover.popoverPresentationController?.sourceRect = button.bounds
present(popover, animated: true)

3.5 Mermaid

Diagramme en cours de génération...

4. SwiftUI : fondations

SwiftUI est déclaratif : le code décrit l'UI, l'état pilote le rendu.

4.1 Première vue

struct ContentView: View {
    var body: some View {
        VStack(spacing: 16) {
            Image(systemName: "cart.fill")
                .font(.system(size: 60))
                .foregroundStyle(.tint)
            Text("Boutique")
                .font(.largeTitle.bold())
            Button("Commencer") {
                // action
            }
            .buttonStyle(.borderedProminent)
        }
        .padding()
    }
}

4.2 Principe

Diagramme en cours de génération...

4.3 HStack / VStack / ZStack

  • HStack : horizontal.
  • VStack : vertical.
  • ZStack : superposition (comme Box en Compose).

5. State & data flow

5.1 @State

État local au View (valeur), propriétaire de la vérité.

struct CounterView: View {
    @State private var count = 0

    var body: some View {
        Button("Compte : \(count)") { count += 1 }
    }
}

5.2 @Binding

Référence vers l'état possédé ailleurs.

struct StepperRow: View {
    @Binding var quantity: Int
    var body: some View {
        Stepper("Quantité", value: $quantity, in: 1...10)
    }
}

struct CartView: View {
    @State private var quantity = 1
    var body: some View {
        StepperRow(quantity: $quantity)
    }
}

5.3 @StateObject / @ObservedObject

  • @StateObject : propriétaire du ObservableObject (créé une fois).
  • @ObservedObject : référence vers un objet externe.
final class CartModel: ObservableObject {
    @Published var items: [Product] = []
    var total: Int { items.reduce(0) { $0 + $1.priceCents } }
}

struct CartView: View {
    @StateObject private var cart = CartModel()
    // …
}

5.4 @EnvironmentObject

Objets partagés injectés dans l'environnement.

@main
struct App: App {
    @StateObject private var cart = CartModel()
    var body: some Scene {
        WindowGroup { ContentView().environmentObject(cart) }
    }
}

struct DetailView: View {
    @EnvironmentObject var cart: CartModel
    // …
}

6. @Observable et @Bindable

6.1 Le macro @Observable (iOS 17+)

Remplace ObservableObject/@Published par un modèle plus simple et performant.

import Observation

@Observable
final class CartModel {
    var items: [Product] = []
    var isCheckingOut = false
}

struct CartView: View {
    @State private var cart = CartModel()
    // accès direct, la vue se met à jour automatiquement
}

6.2 @Bindable

Pour passer un @Bindable d'un @Observable à des champs avec $.

struct EditView: View {
    @Bindable var cart: CartModel
    var body: some View {
        Toggle("Panier actif", isOn: $cart.isCheckingOut)
    }
}

6.3 @Observable vs ObservableObject

ObservableObject@Observable
MacroNonOui
@PublishedOuiNon (accès direct)
@StateObject/@EnvironmentObjectOui@State/@Environment
Perf de suiviPar objetPar propriété
RecommandationLegacyiOS 17+

7. NavigationStack

7.1 NavigationStack (iOS 16+)

struct HomeView: View {
    var body: some View {
        NavigationStack {
            List(products) { product in
                NavigationLink(value: product) {
                    ProductRow(product)
                }
            }
            .navigationTitle("Boutique")
            .navigationDestination(for: Product.self) { product in
                ProductDetailView(product: product)
            }
        }
    }
}
  • NavigationLink(value:) + navigationDestination(for:) : routes typées.
  • Le back est géré par la stack.

7.2 Sheets et fullScreenCover

.sheet(isPresented: $showEditor) {
    EditorView()
        .presentationDetents([.medium, .large])   // iOS 16+
}

7.3 Mermaid : navigation SwiftUI

Diagramme en cours de génération...

8. Animations & previews

8.1 Animations

@State private var expanded = false

Button {
    withAnimation(.spring(response: 0.4, dampingFraction: 0.8)) {
        expanded.toggle()
    }
} label: {
    Text(expanded ? "Réduire" : "Déplier")
}

if expanded {
    Text("Contenu déplié")
        .transition(.opacity.combined(with: .move(edge: .top)))
}

8.2 Exemples de modifiers

  • .animation(.default, value: state) : anime quand state change.
  • .transition(...) : apparition/disparition.
  • .matchedGeometryEffect(id:in:) : transition continue entre vues (like Hero).

8.3 Previews

#Preview("État vide") {
    ProductListView(products: [])
}

#Preview("Avec données") {
    ProductListView(products: sampleProducts)
}

#Preview("Dark mode") {
    ProductListView(products: sampleProducts)
        .preferredColorScheme(.dark)
}

9. Modifiers

Les modifiers transforment une vue. Ordre important.

Text("Boutique")
    .font(.title2.weight(.semibold))
    .foregroundStyle(.primary)
    .padding(12)
    .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 12))
    .accessibilityLabel("Titre de la boutique")

Modifiers réutilisables :

struct CardStyle: ViewModifier {
    func body(content: Content) -> some View {
        content
            .padding()
            .background(.background.secondary)
            .clipShape(RoundedRectangle(cornerRadius: 16))
    }
}

extension View {
    func cardStyle() -> some View { modifier(CardStyle()) }
}

10. Auto Layout : constraints, stacks, priorities

10.1 Constraints programmatiques

NSLayoutConstraint.activate([
    title.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 16),
    title.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 16),
    subtitle.topAnchor.constraint(equalTo: title.bottomAnchor, constant: 8),
    subtitle.leadingAnchor.constraint(equalTo: title.leadingAnchor),
    image.topAnchor.constraint(equalTo: subtitle.bottomAnchor, constant: 16),
])

10.2 Stacks

let stack = UIStackView(arrangedSubviews: [title, subtitle, button])
stack.axis = .vertical
stack.spacing = 12
stack.alignment = .leading
stack.distribution = .fill

10.3 Priorités

  • contentHuggingPriority : résister à l'étirement.
  • contentCompressionResistancePriority : résister à la compression.
label.setContentHuggingPriority(.required, for: .horizontal)
label.setContentCompressionResistancePriority(.required, for: .horizontal)

10.4 SF Symbols et safe areas

Image(systemName: "person.crop.circle")
    .symbolRenderingMode(.hierarchical)

11. Diffable data sources

11.1 Pourquoi ?

reloadData() recalcule tout ; reloadRows indexe à la main. Les diffable data sources calculent les différences automatiquement (c'est ce que fait ListAdapter côté Android).

11.2 Table & collection

Vus au §2 : NSDiffableDataSourceSnapshot décrit l'état complet ; apply anime les différences.

11.3 Supplements (headers)

snapshot.appendSections([.featured, .regular])
collectionView.register(
    HeaderView.self,
    forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader,
    withReuseIdentifier: "Header"
)

12. Résumé et checklist

12.1 Points clés

  • UIKit avancé : UICollectionView + CompositionalLayout, diffable.
  • Navigation : UINavigationController, UITabBarController, modals, popovers.
  • SwiftUI déclaratif : state → view → render.
  • @State/@Binding/@StateObject/@EnvironmentObject ; @Observable moderne.
  • NavigationStack typée ; sheets.
  • Animations implicites via withAnimation + transitions.
  • Previews : une par état.
  • Modifiers : chaîne ordonnée.
  • Auto Layout : anchors, stacks, priorités.
  • Diffable data sources : différences animées.

12.2 Checklist

  • Je construis une table UICollectionView avec layout compositional
  • J'utilise une diffable data source
  • Je gère nav controller, tab bar, modals, popovers
  • Je crée une vue SwiftUI avec @State et @Binding
  • J'utilise @Observable et @Bindable
  • Je navigue avec NavigationStack et navigationDestination
  • J'anime avec withAnimation et transitions
  • J'écris des previews multiples
  • Je gère les priorités de contraintes

Prochain chapitre

08-React-Native : RN new architecture, Expo, navigation, styling, animations, state, OTA, perf.