Almost Netflix: An iOS Netflix Clone built with Appwrite
Tags: ios, showdev, opensource, tutorial
Published: 2022-02-08 09:52:44

Welcome to the fourth post in our Almost Netflix series! We'll be building upon the [project setup](https://dev.to/appwrite/did-we-just-build-a-netflix-clone-with-appwrite-28ok) and build another frontend for our Netflix Clone! In this post, we will take a closer look at building the clone using iOS. In the next post of this series, we'll be building frontend for Android!

This one's all about iOS, so let's get started!

![Cover Image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pxctiryafv43w9fz5vql.png)
 
 
It would be impossible to write every piece of code in this article 😬 you will read about all essential concepts, components, and communication with Appwrite. Still, if you want to check out every corner of our Almost Netflix for iOS, you can check out the [GitHub Source Code](https://github.com/appwrite/demo-almost-netflix-for-apple) that contains the whole app.

 > **🤔 What is Appwrite?**
Appwrite is an open source backend-as-a-service that abstracts all the complexity involved in building a modern application by providing you with a set of REST APIs for your core backend needs. Appwrite handles user authentication and authorization, databases, file storage, cloud functions, webhooks, and much more! If anything is missing, you can extend Appwrite using your favorite backend language.

## 📃 Requirements

In order to continue with this tutorial, you will need the following:

1. Access to an Appwrite project or permission to create one. If you don't already have an Appwrite instance, you can install it following our [official installation guide](https://appwrite.io/docs/installation).
2. Setup Appwrite project following our Almost Netflix [server setup guide](https://dev.to/appwrite/did-we-just-build-a-netflix-clone-with-appwrite-28ok)
3. Access to XCode 12 or newer. Find more about Xcode [here](https://developer.apple.com/xcode).
4. Know the basics of iOS development and Swift UI

## 🛠️ Create iOS project

We will start by creating a new project. Open Xcode and select **Start New Project**. On the next screen select **iOS -> App**, then click **Next**.

![Create Project](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zgf1wottaz23td2knvle.png)

On the next screen, give your project a name, enter an organization id and for the interface, select SwiftUI with language set to Swift, then click next.

![Name the project](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d1vd2u34zqmmkmwuvmzp.png)

On the next screen, select the folder where you want to save your new project and click create. This will create a new project and open it in Xcode. You should now see the following screen.

## ↪️ Dependencies

We will start by adding dependencies. For this project we need two packages. First the `Appwrite SDK` and then `Kingfisher` image package for displaying images from URL. In order to add a package, go to **File -> Add Packages**

In the dialog box that appears, tap the top right search icon and type the following GitHub URL for the SDK **https://github.com/appwrite/sdk-for-apple** and hit **Enter**. You should see the **sdk-for-apple** package listed.

![Appwrite SDK search](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x1fkvbs8m44khsqo16u5.png)

Now select the **sdk-for-apple** package and on the right side, select dependency rule as **Up to major version** and use `version 0.3.0` for the latest SDK. Now click on the **Add Package** button. Xcode will download the **Appwrite Apple SDK** along with its dependencies and will add it to your project.

![Add Package to Target](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ka2q5s6eupfnbhd3q4id.png)

Make sure the proper target is selected in the **Add to target** in the dialog as shown above, then click **Add Package** button. The package should successfully be added to your project. Now following the same process, this time search for the `Kingfisher` package using the GitHub URL as the following **https://github.com/onevcat/Kingfisher**.

Now that we have all our dependencies, we will create an [`AppwriteService`](https://github.com/appwrite/demo-almost-netflix-for-apple/blob/main/AlmostNetflix/Utilities/Appwrite.swift) class and initialize the Appwrite client, database and authentication services. Let's start. First add a new Swift file and name it `AppwriteService.swift` containing the following:

```swift
class AppwriteService {
    let client: Client
    let database: Database
    let account: Account
    let storage: Storage
    let avatars: Avatars
    
    static let shared = AppwriteService()
    
    init() {
        client = Client()
            .setEndpoint("https://YOUR_ENDPOINT")
            .setProject("PROJECT_ID")
            .setSelfSigned() // Do not use in production
        
        database = Database(client)
        account = Account(client)
        storage = Storage(client)
        avatars = Avatars(client)
    }
}
```

Using the above code, we are initializing our client and database, account, storage and avatars services. Replace `YOUR_ENDPOINT` with your own endpoint and `PROJECT_ID` with your own project ID. Now that we have setup our Appwrite client and services, we will start implementing authentication in our application.

## 🔐 Authentication

We will start by creating basic views that are required to implement the authentication flow. Let's start with login view. First create a new **SwiftUI View** file and name it `LoginView.swift`. The full view definition can be found [here](https://github.com/appwrite/demo-almost-netflix-for-apple/blob/main/AlmostNetflix/Views/LoginView.swift), for now we will focus on the button actions, where they defer to a view model we will create later:
```swift
    @State private var email = ""
    @State private var password = ""  
    @EnvironmentObject var authVM: AuthVM
    
    var body: some View {
        VStack {
            ...
            
            Button("Login") {
                authVM.login(email: email, password: password)
            }

            ...
        }
    }
}
```

Create a similar file named `SignupView.swift` and update with the full view from [here](https://github.com/appwrite/demo-almost-netflix-for-apple/blob/main/AlmostNetflix/Views/SignupView.swift). This view is largely the same but calls a different method in our `AuthVM` when the **Sign Up** button is clicked.

We now need a view that we can use to switch between login and sign up. Create a new file called [`TitleView.swift`](https://github.com/appwrite/demo-almost-netflix-for-apple/blob/main/AlmostNetflix/Views/TitleView.swift). Here we will focus on the navigation aspect:

```swift
import Foundation
import SwiftUI

struct TitleView: View {
    
    @State private var selection: String? = nil
    
    var body: some View {
        NavigationView {
            VStack {
                
                ...
                
                NavigationLink(destination: LoginView(), tag: "Sign In", selection: $selection) {}
                NavigationLink(destination: SignupView(), tag: "Sign Up", selection: $selection) {}
                
                Button("Sign In") {
                    self.selection = "Sign In"
                }
                
                Button("Sign Up") {
                    self.selection = "Sign Up"
                }
                
                ...
            }
        }
    }
}
```

Here we use navigation links inside a navigation view to navigate to sign in or sign up. When a user taps one of the buttons, the `selection` state variable is set. This way, if we were to add more buttons, we only need to add new tags, not a new state variable for each button.

We now have a nice title view that navigates to either login or sign up that looks like this:

![Title View Display](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y23d55hdjt9txqu4oa93.png)

Now we need a view to navigate to on successful sign in or sign up. Create another file named [`HomeView.swift`](https://github.com/appwrite/demo-almost-netflix-for-apple/blob/main/AlmostNetflix/Views/HomeView.swift) and update with the following code for now, to simply create a dummy home page to make sure the auth flow is complete and correct.

```swift
import SwiftUI

struct HomeView: View {
    
    var body: some View {
        NavigationView {
            ZStack {
                Color(.black).ignoresSafeArea()
            }
        }
    }
}
```

We also need a view that will display either a splash UI, login UI or the home page based on authentication state. Let's create a file named [`MainView.swift`](https://github.com/appwrite/demo-almost-netflix-for-apple/blob/main/AlmostNetflix/Views/MainView.swift) and update with the following code.

```swift
import SwiftUI

struct MainView: View {
    
    @EnvironmentObject var authVM: AuthVM

    var body: some View {
        Group {
            if !authVM.checkedForUser {
                SplashView()
            } else if authVM.user != nil {
                HomeView().environmentObject(MoviesVM(userId: authVM.user!.id))
            } else {
                TitleView()
            }
        }
    }
}
```

Now that we have all the views necessary for authentication flow, we will create a view model that will handle the authentication logic. Create a new swift file and name it [`AuthVM.swift`](https://github.com/appwrite/demo-almost-netflix-for-apple/blob/main/AlmostNetflix/ViewModels/AuthVM.swift). Inside the file create the following class.

```swift
class AuthVM: ObservableObject {
    @Published var checkedForUser = false
    @Published var error: String?
    @Published var user: User?
}
```

We also have defined three variables that will help us track and display the authentication state, error and user's details. Now let's add a function that will help us create an account. Add the following to the `AuthVM` class.

```swift
func create(name: String, email: String, password: String) {
    error = ""
    AppwriteService.shared.account.create(userId: "unique()", email: email, password: password, name: name) { result in
        switch result {
        case .failure(let err):
            DispatchQueue.main.async {
                print(err.message)
                self.error = err.message
            }
        case .success:
            self.login(email: email, password: password)
        }
    }
}
```

Above, we have a create method that accepts name, email and password and creates a new account using Appwrite's account service. Upon successful creation of an account, we are also calling login method to create session for the user. Let us now add the following `login` function to our `AuthVM`.

```swift
public func login(email: String, password: String) {
    error = ""
    AppwriteService.shared.account.createSession(email: email, password: password) { result in
        switch result {
        case .failure(let err):
            DispatchQueue.main.async {
                self.error = err.message
            }
        case .success:
            self.getAccount()
        }
    }
}
```

The login function accepts an email and password and creates a session using Appwrite's account service. Once a session is created, we call the `getAccount` function to get the details of the current user. Next, let us add the `getAccount` method that will get the user's account and help us check whether the user is logged in.

```swift
private func getAccount() {
    error = ""
    AppwriteService.shared.account.get() { result in
        DispatchQueue.main.async {
            self.checkedForUser = true

            switch result {
            case .failure(let err):
                self.error = err.message
                self.user = nil
            case .success(let user):
                self.user = user
            }
            
        }
    }
}
```

To get the user account we are simply calling the `get` function of the Appwrite's account service. Upon success we are setting the user property with the received user details. Finally let us add an initializer method to the `AuthVM` class that will call `getAccount` so that user's authentication state is checked on application start-up.

```swift
init() {
    getAccount()
}
```

That's it for authentication! Our application will now show a splash screen until we have checked if the user is logged in, if they are, they will be shown the home screen, otherwise the login screen.

## 🎬 Movies Page 

![Feed View](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4w0i9lfkkn1t9ndykdqf.png)
 
Let us head back to the `HomeView.swift` file and start creating our movie carousel views. We need to create a horizontal scroll view within a vertical scroll view. The vertical scroller will iterate movie categories and the horizontal scrollers will iterate the movies within each category. We will first add the vertical scroll to our `HomeView` as follows:

```swift
    ScrollView(.vertical, showsIndicators: false) {
        if(moviesVM.featured != nil) {
            MovieItemFeaturedView(
                movie: moviesVM.featured!,
                isInWatchlist: moviesVM.watchList.contains(moviesVM.featured!.id),
                onTapMyList: {
                    self.onTapMyList(moviesVM.featured!.id)
                }
            )
        } else if(!((moviesVM.movies["movies"] ?? []).isEmpty)) {
            let movie = (moviesVM.movies["movies"]!).first!
            
            MovieItemFeaturedView(
                movie: movie,
                isInWatchlist: moviesVM.watchList.contains(movie.id),
                onTapMyList: {
                    self.onTapMyList(movie.id)
                }
            )
        }
        
        VStack(alignment: .leading, spacing: 16) {
            ForEach(appwriteCategories) { category in
                MovieCollection(title: category.title, movies: moviesVM.movies[category.id] ?? [])
                    .frame(height: 180)
            }
        }.padding(.horizontal)
    }
```

Let's break that down. Inside our scroll view, we first check if we have a featured movie. If we do, this is shown at the top of the page using a `MovieItemFeaturedView` that we will add later.

Below the featured movie, we have a vertical stack that iterates some pre-defined categories and displays a `MovieCollectionView` for each.

Now we can create our subviews. First, create a new file called [`MovieItemFeaturedView.swift`](https://github.com/appwrite/demo-almost-netflix-for-apple/blob/main/AlmostNetflix/Views/MovieItemFeaturedView.swift) and add the following:

```swift
import SwiftUI
import Kingfisher

struct MovieItemFeaturedView: View {
    @State private var isShowingDetailView = false

    let movie: Movie
    let isInWatchlist: Bool
    let onTapMyList: () -> Void
    
    var body: some View {
        ZStack{
            KFImage.url(URL(string: movie.imageUrl))
                .resizable()
                .scaledToFill()
                .clipped()

            VStack {
                HStack {
                    ...

                    Button {
                        onTapMyList()
                    } label: {
                        VStack {
                            Image(systemName: self.isInWatchlist ? "checkmark" :"plus")
                            Text("My List")
                        }
                    }
                    
                    NavigationLink(destination: MovieDetailsView(movie: movie), isActive: $isShowingDetailView) { EmptyView() }

                    Button {
                        self.isShowingDetailView = true
                    } label: {
                        VStack {
                            Text("Info")
                        }
                    }
                }
            }
        }
    }
}
```

Our featured movie is now set-up, so let us move on to the `MovieCollectionView`. Create a new filed called [`MovieCollectionView.swift`](https://github.com/appwrite/demo-almost-netflix-for-apple/blob/main/AlmostNetflix/Views/MovieCollectionView.swift) and update with the following code:

```swift
    ScrollView(.horizontal, showsIndicators: false) {
        HStack {
            ForEach(movies) { movie in
                MovieItemThumbnailView(movie: movie)
                    .frame(width: itemWidth, height: itemHeight)
            }
        }
        .frame(height: frameHeight)
    }
```

Here we have a scroll view, with a horizontal stack inside, which iterates each movie within the given collection and displays a `MovieItemThumbnailView` for each. Now we can create a new file, [`MovieItemThumbnailView.swift`](https://github.com/appwrite/demo-almost-netflix-for-apple/blob/main/AlmostNetflix/Views/MovieItemThumbnailView.swift) and add the following:

```swift
    NavigationLink (destination: MovieDetailsView(movie: movie)) {
        KFImage.url(URL(string: movie.imageUrl))
            .resizable()
            .scaledToFit()
            .cornerRadius(4)
    }
```

Here our image is nested inside a navigation link, so when the image is clicked, our app will navigate to the movie detail view. That's it for our movie feed. We now have a nested scroll view displaying collections of movies by category. Now we need to create a detail view for individual movies.

## 🕵️ Detail Page

Now we can define our movie detail view. Create a new file called [`MovieDetailsView.swift`](https://github.com/appwrite/demo-almost-netflix-for-apple/blob/main/AlmostNetflix/Views/MovieDetailsView.swift). Inside the details view we have a button that toggles adding the current movie to a users watchlist:

```swift
    Button {
        self.addToMyList()
    } label: {
        VStack {
            Image(systemName: moviesVM.watchList.contains(movie.id) ? "checkmark" : "plus")
            Text("My List")
        }
        .padding()
    }
      
    func addToMyList() -> Void {
        moviesVM.addToMyList(movie.id)
    }
}
```

Now we need to add the `MovieGridView`, which shows similar movies to the one displayed on the detail page. Let's add that now as a new file called [`MovieGridView.swift`](https://github.com/appwrite/demo-almost-netflix-for-apple/blob/main/AlmostNetflix/Views/MovieGridView.swift):

```swift
    LazyVGrid(columns: [GridItem(.adaptive(minimum: 100))], alignment: .leading, spacing: 4) {
        ForEach(movies) { movie in
            MovieItemThumbnailView(movie: movie)
        }
    }
```

Our movie detail view will now look like the following:

![Movie Detail View](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7z22ukwv7hv0akqp9you.png)
 

## ⚙️ Movies ViewModel

We need to add the **View Model** we are using to fetch movies and publish them to the UI. This class contains most of the logic for managing the movie feed as well as the currently logged in user's watchlist. Add a new file, [`MoviesVM.swift`](https://github.com/appwrite/demo-almost-netflix-for-apple/blob/main/AlmostNetflix/ViewModels/MoviesVM.swift) with the following:

```swift
class MoviesVM: ObservableObject {
    @Published var featured: Movie?
    @Published var movies: [String:[Movie]] = [:]
    @Published var watchList: [String] = []
    
    let userId: String
    
    init(userId: String) {
        self.userId = userId
        getMovies()
        getFeatured()
    }
    
    public func getMyWatchlist() {
        AppwriteService.shared.database.listDocuments(
            collectionId: "movies",
            queries: [
                Query.equal("$id", value: watchList)
            ]
        ) { result in
            DispatchQueue.main.async {
                switch result {
                case .failure(let err):
                    print(err.message)
                case .success(let docs):
                    self.movies["watchlist"] = docs.convertTo(fromJson: Movie.from)
                }
            }
        }
    }
    
    func addToMyList(_ movieId: String) {
        if(self.watchList.contains(movieId)) {
            removeFromMyList(movieId)
        } else {    
            AppwriteService.shared.database.createDocument(collectionId: "watchlists", documentId: "unique()", data: ["userId": userId, "movieId": movieId, "createdAt": Int(NSDate.now.timeIntervalSince1970)], read: ["user:\(userId)"], write: ["user:\(userId)"]){ result in
                DispatchQueue.main.async {
                    switch result {
                    case .failure(let err):
                        print(err.message)
                    case .success(_):
                        self.watchList.append(movieId)
                        self.getMyWatchlist()
                        print("successfully added to watchlist")
                    }
                    
                }
            }
        }
    }

    func removeFromMyList(_ movieId: String) {
        AppwriteService.shared.database.listDocuments(collectionId: "watchlists", queries: [
                Query.equal("userId", value: userId),
                Query.equal("movieId", value: movieId)
            ], limit: 1) { result in
                switch result {
                case .failure(let err):
                    print(err.message)
                case .success(let docList):
                    AppwriteService.shared.database.deleteDocument(collectionId: "watchlists", documentId: docList.documents.first!.id) {result in
                        DispatchQueue.main.async {
                            switch result {
                            case .failure(let err):
                                print(err.message)
                            case .success(_):
                                let index = self.watchList.firstIndex(of: movieId)
                                if(index != nil) {
                                    self.watchList.remove(at: index!)
                                    self.getMyWatchlist()
                                    print("removed from watchlist")
                                }
                            }
                        }
                    }
                }
            }
    }
    
    func isInWatchlist(_ movieIds: [String]) {
        AppwriteService.shared.database.listDocuments(
            collectionId: "watchlists",
            queries: [
                Query.equal("userId", value: userId),
                Query.equal("movieId", value: movieIds)
            ]
        ) { result in
            DispatchQueue.main.async {
                switch result {
                case .failure(let err):
                    print(err.message)
                case .success(let docList):
                    let docs = docList.convertTo(fromJson: Watchlist.from)
                    for doc in docs {
                        self.watchList.append(doc.movieId)
                    }
                    if(docs.count > 1) {
                        self.getMyWatchlist()
                    }
                }
                
            }
        }
    }
    
    private func getFeatured() {
        AppwriteService.shared.database.listDocuments(
            collectionId: "movies",
            limit: 1,
            orderAttributes: ["trendingIndex"],
            orderTypes: ["DESC"]
        ) { result in
            DispatchQueue.main.async {
                switch result {
                case .failure(let err):
                    print(err.message)
                case .success(let docs):
                    self.featured = docs.convertTo(fromJson: Movie.from).first
                    if(self.featured != nil) {
                        self.isInWatchlist([self.featured!.id])
                    }
                }
                
            }
        }
    }
    
    private func getMovies() {
        appwriteCategories.forEach {category in
            AppwriteService.shared.database.listDocuments(collectionId: "movies", queries: category.queries, orderAttributes: category.orderAttributes, orderTypes: category.orderTypes) { result in
                DispatchQueue.main.async {
                    switch result {
                    case .failure(let err):
                        print(err.message)
                    case .success(let docs):
                        self.movies[category.id] = docs.convertTo(fromJson: Movie.from)
                        self.isInWatchlist(docs.documents.map { $0.id });
                    }
                    
                }
            }
        }
    }
}

```

## 🔖 Watchlist Page

Bonus points: We have also implemented a **Watchlist** feature. To add this, we need to another new view, [`WatchlistView.swift`](https://github.com/appwrite/demo-almost-netflix-for-apple/blob/main/AlmostNetflix/Views/WatchlistView.swift): 

```swift
    ScrollView(.vertical, showsIndicators: false) {
        if(!(moviesVM.movies["watchlist"] ?? []).isEmpty) {
            MovieGridView(movies: moviesVM.movies["watchlist"] ?? [])
        } else {
            Text("You have no items in your watchlist")
                .foregroundColor(Color.white)
        }
    }
```

This view will get all the movies in the users watchlist and display them in our previously created `MovieGridView`.

## 👨‍🎓 Conclusion

Ta-da! We have (almost) cloned Netflix, *Swiftly* and easily with Appwrite as our backend! To become part of the Appwrite community, you can join our [Discord](https://appwrite.io/discord) server. We look forward to seeing what you build and who you are (when you join our discord!)

With each Appwrite release amazing new features are being added and as they are, we will return to our Netflix clone to keep it growing! 

## 🔗 Learn more

- [Getting Started with Apple](https://appwrite.io/docs/getting-started-for-apple)
- [SwiftUI Playground](https://github.com/appwrite/playground-for-apple-swiftui)
- [Appwrite Github](https://github.com/appwrite)
- [Appwrite Documentation](https://appwrite.io/docs)