iOS TrainingiOS Training
GitHub
GitHub
  • Welcome
  • Presentation
  • Swift (part 1)
  • Swift (part 2)
  • SwiftUI
  • Data manipulation
  • Mini project
  • Going further

Mini project

The final chapter of this training will ask you to create a SwiftUI app from scratch.

Requirements

The app consists of a movie explorer app with the following features:

  • Search for movies by title.
  • View the details of the selected movie.
  • The app requires the user to be logged in.
  • The app allows a new user to register.
  • The movie list screen allows to logout from the app.
  • The app remembers the logged in user after a restart.
  • The app uses this API for the authenticating and searching for movies.
    • The /movies/search endpoint requires to pass the token retrieved from endpoint /user/login or user/register in this header: Authorization: Bearer \(userResponse.token)
  • (Optional) The result of previous queries is locally cached.
  • (Optional) Add movie to local favorites ⭐️
  • (Optional) Animate the transition between the login view and the movie list view (tutorial).

A preview of the app can be seen here.

API limit considerations

To avoid reaching the OMDb API limit, the backend API used in this project is configured to return a fixed set of movie details for any search query. This means that some results will miss some fields. The code below shows how to handle optional fields in the Movie struct.

struct Movie: Codable {
    let title: String
    let released: String?
    let director: String?
    let actors: String?
    let poster: String
    let plot: String?
    let metascore: String?
}

Hints

  • There are many techniques to handle the flow from the login view to the movie list view. On of them is to rely on a logged state. The following gives an overview how it looks like.
struct ContentView: View {
    @State var loggedIn: false
    
    var body: some View {
        if loggedIn {
            MovieListView()
        } else {
            // The LoginView takes a callback that is called when the login succeeds
            LoginView { newLoggedIn in
                loggedIn = newLoggedIn
            }
        }
    }
}
  • In the login view, use an enum to track the state of the login operation so that you can disable the login button when a request is running.
enum LoginState {
    case neutral, loading, success, failure
}
struct LoginView: View {
    @State private var loginState: LoginState = .neutral
    // other code
}
  • Use a Task object to run async code.
Button("Login") { 
    loginState = .loading
    Task {
        if await login() {
            onLoginSuccess(true)
        }
    }
}

Swift Concurrency crashes on Swift Playground

Do not use the Swift Playground app to run you app as it does not work well with SwiftUI + Swift Concurrency (async, await and Task). Instead, you can create an Xcode project of type Playground to combine the power of Xcode and the simplicity of Playground projects.

  • Use DebouncedOnChange Swift package to optimize search.
  • To generate the initial code for a preview, open a view and then use the Xcode feature Editor -> Create preview
  • The List view requires that you specify an id field List(movies, id: \.title) or that the items conform to Identifiable protocol
  • If you can't add SwiftPM packages from Xcode, add them by editing the package.swift file by hand. Here is an example below.
// swift-tools-version: 6.0

// WARNING:
// This file is automatically generated.
// Do not edit it by hand because the contents will be replaced.

import PackageDescription
import AppleProductTypes

let package = Package(
    name: "Moovy",
    platforms: [
        .iOS("18.0"),
        .macOS("14.0")
    ],
    products: [
        .iOSApplication(
            name: "Moovy",
            targets: ["AppModule"],
            displayVersion: "1.0",
            bundleVersion: "1",
            appIcon: .placeholder(icon: .sun),
            accentColor: .presetColor(.cyan),
            supportedDeviceFamilies: [
                .pad,
                .phone
            ],
            supportedInterfaceOrientations: [
                .portrait,
                .landscapeRight,
                .landscapeLeft,
                .portraitUpsideDown(.when(deviceFamilies: [.pad]))
            ],
            capabilities: [
                .outgoingNetworkConnections()
            ],
            appCategory: .entertainment
        )
    ],
    dependencies: [
        .package(url: "https://github.com/Tunous/DebouncedOnChange.git", from: "2.0.0"),
        .package(url: "https://github.com/kishikawakatsumi/KeychainAccess.git", "4.0.0"..<"5.0.0")
    ],
    targets: [
        .executableTarget(
            name: "AppModule",
            dependencies: [
                "DebouncedOnChange",
                "KeychainAccess"
            ],
            path: "."
        )
    ],
    swiftLanguageVersions: [.version("6")]
)
Edit this page
Last Updated: 10/3/25, 1:53 PM
Contributors: YBE WL, Yassine Benabbas, yostane
Prev
Data manipulation
Next
Going further