Chapitre 7
07 - iOS UI
07 - iOS UI
07 - iOS UI : Cours complet
Table des matières
- UIKit avancé
- UICollectionView & UITableView
- Navigation controllers, tab bars, modals, popovers
- SwiftUI : fondations
- State & data flow
- @Observable et @Bindable
- NavigationStack
- Animations & previews
- Modifiers
- Auto Layout : constraints, stacks, priorities
- Diffable data sources
- 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 | |
|---|---|---|
| Macro | Non | Oui |
| @Published | Oui | Non (accès direct) |
| @StateObject/@EnvironmentObject | Oui | @State/@Environment |
| Perf de suivi | Par objet | Par propriété |
| Recommandation | Legacy | iOS 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.