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)
- The /movies/search endpoint requires to pass the token retrieved from endpoint /user/login or user/register in this header:
- (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
fieldList(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")]
)