iosNudge

Image Carousel

MediumSwiftUISwiftUIConcurrencyAccessibility 45 min read

Build an image carousel, like the "Featured" banner at the top of many shopping apps. One slide fills the width. The user swipes left and right. Every 3 seconds it moves to the next slide by itself.

struct ImageCarousel: View {
    let slides: [Slide]
    var interval: Duration = .seconds(3)
}

The slides use colors and SF Symbols, so you need no image files. SlideView is done for you.

Example flow

  1. The carousel shows slide 1 of 5. Dot 1 is highlighted.
  2. After 3 seconds it slides to slide 2. Dot 2 is highlighted.
  3. The user puts a finger down and drags slowly. Nothing moves by itself during the drag.
  4. The user lets go on slide 4. The 3-second wait starts again from zero.
  5. After slide 5 it goes back to slide 1.

Requirements

  • Horizontal paging: each swipe moves exactly one slide.
  • Page dots below the slides. The current page is highlighted.
  • Tapping a dot jumps to that slide.
  • Auto-advance every interval. After the last slide, go back to the first.
  • Pause while the user drags. Restart the wait after the drag ends.
  • A swipe by the user also restarts the wait. The next auto move is a full 3 seconds later.
  • A play/pause button.
  • Stop the timer when the view goes away. No leaks.

What a good answer handles

  • Zero or one slide. No crash. No timer for one slide.
  • Reduce Motion. If the user turned on Reduce Motion, do not auto-advance and do not animate.
  • VoiceOver. Do not move slides under a VoiceOver user. The dots work like UIPageControl: one element, "Page, 2 of 5", swipe up or down to change the page.
  • No Timer leaks. Explain how your timer stops.

Notes

  • Target iOS 18 or later. onScrollPhaseChange (iOS 18) tells you when the user is dragging. Say how you would do it on iOS 17.
  • Use ScrollView with .scrollTargetBehavior(.paging) and .scrollPosition(id:), or TabView with .tabViewStyle(.page). Explain your choice.

Starter code

Copy this into a new Xcode project (or Swift Playgrounds) and build from here.

import SwiftUI

// iOS 18+ (you may use onScrollPhaseChange).

// MARK: - Model (done for you)

struct Slide: Identifiable, Hashable {
    let id: Int
    let title: String
    let symbol: String
    let color: Color
}

extension Slide {
    static let samples: [Slide] = [
        Slide(id: 0, title: "Mountains", symbol: "mountain.2.fill", color: .blue),
        Slide(id: 1, title: "Beach", symbol: "beach.umbrella.fill", color: .orange),
        Slide(id: 2, title: "City", symbol: "building.2.fill", color: .purple),
        Slide(id: 3, title: "Forest", symbol: "tree.fill", color: .green),
        Slide(id: 4, title: "Night Sky", symbol: "moon.stars.fill", color: .indigo),
    ]
}

// MARK: - Carousel

struct ImageCarousel: View {
    let slides: [Slide]
    var interval: Duration = .seconds(3)

    @State private var currentID: Slide.ID?

    var body: some View {
        VStack(spacing: 8) {
            // TODO: a horizontal pager, one slide per page.
            if let first = slides.first {
                SlideView(slide: first)
                    .frame(height: 220)
            }

            // TODO: page dots, auto-advance every `interval`, pause while dragging,
            // a play/pause button, accessibility.
        }
    }
}

// MARK: - Slide (done for you)

struct SlideView: View {
    let slide: Slide

    var body: some View {
        RoundedRectangle(cornerRadius: 20)
            .fill(slide.color.gradient)
            .overlay {
                VStack(spacing: 12) {
                    Image(systemName: slide.symbol)
                        .font(.system(size: 60))
                    Text(slide.title)
                        .font(.title2.bold())
                }
                .foregroundStyle(.white)
            }
            .padding(.horizontal)
            .accessibilityElement(children: .ignore)
            .accessibilityLabel(slide.title)
            .accessibilityAddTraits(.isImage)
    }
}

#Preview {
    VStack(alignment: .leading) {
        Text("Featured").font(.title.bold()).padding(.horizontal)
        ImageCarousel(slides: Slide.samples)
        Spacer()
    }
}

Solution

Try to build it first. Then compare your design with ours.