Let's Build A Job Portal With iOS
Tags: swift, tutorial, ios, news
Published: 2021-10-21 18:25:51

Hey 👋 iOS devs, if you hadn't heard, Appwrite 0.11 has just released with platform wide Apple support. So, what is Appwrite? Appwrite is an open-source self-hosted end-to-end backend as a service for web and mobile applications. Learn more at [Appwrite.io](https://appwrite.io). In this tutorial, we are going to build a job portal application for iOS with SwiftUI using Appwrite as the back-end service. So let's get started.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9rygqpwhyg71gpk194dj.gif)

## 📝 Technical Requirement

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

1. Access to an Appwrite project or permission to create one. If you don't already have an Appwrite instance, you can always install it using our [official installation guide](https://appwrite.io/docs/installation).
2. Access to XCode 12 or newer. Find more about Xcode [here](https://developer.apple.com/xcode)

## 🛠️ 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 new project](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/b444gxo9gaahhu8dq6v6.png)
 

On the next screen, give your project a name, organization id and in interface select SwiftUI and language Swift, then click next.

![Name the project](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9p1gr31lcth1ruoq1u9j.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.

![Project opened](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wmvcuido8cnqheo8acng.png)
 
## 🔐 Authentication

We will start by implementing authentication. For that we will create a LoginView, AuthViewModel and AppwriteService. In order to create a view just hit `cmd+N` on your keyboard. You will be presented with the new file dialog.

![New view](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/iph1ynma415uvylsf5fj.png)
 
There select **SwiftUI View** option and click next. In the next dialog box rename your view to **LoginView** and click create button to create the view.

![Create login view](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ihz85gt9r5yppxlioaet.png)
 
Update the code of the **LoginView** to the following.

```swift
import SwiftUI

struct LoginView: View {
    @State private var email = ""
    @State private var password = ""
    @State private var isActiveSignup = false

    @EnvironmentObject var authVM: AuthViewModel

    var body: some View {
        NavigationView {
            VStack {
                NavigationLink(destination: SignupView(), isActive: $isActiveSignup) {
                    EmptyView()
                }
                HStack {
                    Text("Welcome back to\nAppwrite Jobs")
                        .font(.largeTitle)
                        .padding(.top, 60)
                        .multilineTextAlignment(.leading)
                    Spacer()
                }
                Spacer().frame(height: 10)
                HStack {
                    Text("Let's sign in.")
                        .font(.title)
                    Spacer()
                }
                .padding(.bottom, 30)

                TextField("E-mail", text: self.$email)
                    .padding()
                    .background(Color.gray.opacity(0.2))
                    .cornerRadius(16.0)

                SecureField("Password", text: self.$password)
                    .padding()
                    .background(Color.gray.opacity(0.2))
                    .cornerRadius(16.0)

                Spacer().frame(height: 16)

                Button("Login") {
                    authVM.login(email: email, password: password)
                }
                .foregroundColor(.white)
                .padding()
                .frame(width: 300, height: 50)
                .background(Color.pink)
                .cornerRadius(16.0)

                HStack {
                    Text("Anonymous Login")
                        .onTapGesture {
                            authVM.loginAnonymous()
                        }
                    Text(".")
                    Text("Signup")
                        .onTapGesture {
                            isActiveSignup = true
                        }
                }
                .padding(.top, 30)
                Spacer()

            }
            .foregroundColor(.white)
            .padding([.leading, .trailing], 40)
            .navigationBarTitleDisplayMode(.inline)
            .navigationBarHidden(true)
        }
    }
}

struct LoginView_Previews: PreviewProvider {
    static var previews: some View {
        LoginView()
            .preferredColorScheme(.dark)
    }
}
```

This will create a simple login view as the following.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/huc5jrui23rms0g9uqu8.png)

Similarly let's create **SignupView** and update with the following code.

```swift
import SwiftUI

struct SignupView: View {
    @State private var email = ""
    @State private var password = ""
    @State private var name = ""

    @EnvironmentObject var authVM: AuthViewModel
    @Environment(\.presentationMode) var presentationMode

    var body: some View {
        VStack {
            HStack {
                Image("back-icon")
                    .resizable()
                    .frame(width: 24, height: 21)
                    .onTapGesture {
                        presentationMode.wrappedValue.dismiss()
                    }
                Spacer()
            }
            .padding([.top, .bottom], 30)

            HStack {
                Text("Join\nAppwrite jobs")
                    .font(.largeTitle)
                Spacer()
            }

            Spacer().frame(height: 10)

            HStack {
                Text("Create an account")
                    .font(.title)
                    .padding(.bottom)
                Spacer()
            }
            .padding(.bottom, 30)

            TextField("Name", text: self.$name)
                .padding()
                .background(Color.gray.opacity(0.2))
                .cornerRadius(16.0)

            TextField("E-mail", text: self.$email)
                .padding()
                .background(Color.gray.opacity(0.2))
                .cornerRadius(16.0)

            SecureField("Password", text: self.$password)
                .padding()
                .background(Color.gray.opacity(0.2))
                .cornerRadius(16.0)
            Spacer().frame(height: 16)
            Button("Create account") {
                authVM.create(name: name, email: email, password: password)
            }
            .foregroundColor(.white)
            .padding()
            .frame( maxWidth: .infinity, maxHeight: 60)
            .background(Color.pink)
            .cornerRadius(16.0)

            Spacer()
        }
        .padding([.leading, .trailing], 27.5)
        .navigationBarHidden(true)
    }
}

struct SignupView_Previews: PreviewProvider {
    static var previews: some View {
        SignupView()
            .preferredColorScheme(.dark)
    }
}
```

This will create a simple signup view as the following.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1q1v30fkl4rjwoa1o8s6.png)
 
Also create **HomeView** where later we will display the list of jobs.

```swift
import SwiftUI

struct HomeView: View {
    @EnvironmentObject var authVM: AuthViewModel

    var body: some View {
        VStack {
            HStack {
                Text("Appwrite Jobs")
                    .font(.title)
                    .fontWeight(.bold)
                Spacer()
                Text("Logout")
                    .onTapGesture {
                        authVM.logout()
                    }
            }
            .padding(.top, 40)
            .padding(.horizontal, 36)

            ScrollView {
                HStack {
                    Text("Find your dream\njobs")
                        .font(.largeTitle)
                    Spacer()
                }
                .padding(.vertical, 30)
                .padding(.horizontal, 36)

                Text("Hello World")
            }
        }
    }
}

struct HomeView_Previews: PreviewProvider {
    static var previews: some View {
        HomeView()
            .preferredColorScheme(.dark)
    }
}
```

Also create **MainView** and update with the following code

```swift
import SwiftUI

struct MainView: View {
    @EnvironmentObject var authVM: AuthViewModel

    var body: some View {
        Group {
            if authVM.isLoggedIn {
                HomeView()
            } else {
                LoginView()
            }
        }
        .animation(.easeInOut)
        .transition(.move(edge: .bottom))
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        MainView()
    }
}
```

This is a simple view that displays either **LoginView** or **HomeView** based on authentication state.

Next, let's create our **AuthViewModel**. Hit `cmd+N` again. This time select **Swift File** and click next.

![New swift file](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dnlyzc50rw6w064fsn00.png)
 
Now name your file **AuthViewModel** and click create.

Following the above steps, create another swift file and name it **AppwriteService**.

This is a simple class responsible for initializing the Appwrite SDK and exposing the account service for use in the authentication view model.

Let us also organize our code files into groups. In the left sidebar right click on **Appwrite Jobs** and select `New Group`.

![New group](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cmihd7k4ufdxowlvp0r2.png)
 

Rename the newly created group into **Views**. Follow the same process to create another group called **ViewModels**. Now drag and drop the view files and view model files to the respective groups. Your project explorer should look like the following.

![Grouped](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6sqjf34s1dp3m1e30dhu.png)
 
Before we can continue, we first need to add the Appwrite Swift SDK as a dependency. To do that, go to File -> Add Packages.

![Add Package](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nv0vvwto0a9yhsghzos6.png)
 
In the new dialog that appears, tap the top right search icon and type the GitHub URL for the SDK **https://github.com/appwrite/sdk-for-apple** and hit Enter. You should see the **sdk-for-apple** package listed.

![Search Package](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/r9v3s1vpr1exsjsvwxqf.png)
 
Now select the **sdk-for-apple** package and on the right side, select dependency rule as **Branch*, then choose from `main` for stable code or `dev` for the latest code. Now click on the Add Package button. Xcode will download the Appwrite Swift SDK along with its dependencies and will be added to your project.

![Add package complete](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jr380wcvgirdm7aj8nkz.png)
 
Make sure the proper target is selected in the Add to target view, then click Add Package button. The package should successfully be added to your project.

Once we have the package, now we can initialize our Appwrite SDK. To do that, update **AppwriteService** class with the following code.

```swift
import Foundation

import Appwrite

class AppwriteService {
    private(set) var client: Client
    private(set) var account: Account
    
    static let shared = AppwriteService()
    
    init() {
        client = Client()
            .setEndpoint("YOUR_ENDPOINT")
            .setProject("YOUR_PROJECT_ID")
        
        account = Account(client: client)
    }
}
```

Here, we are creating an Appwrite service class that has client and account variables. We also have a public static shared instance of AppwriteService class to make it easily accessible.

In the init method we are initializing our SDK and then the account service. To initialize the SDK, you need to instantiate the Client object and set at least the endpoint and project ID, both of which can be obtained from the Appwrite console, which we will look into in the next section.

Now that we have our SDK initialized and ready and account service instantiated, let us update the **AuthViewModel** with the following code.

```swift
import Foundation
import Appwrite

class AuthViewModel: ObservableObject {
    @Published var isLoggedIn = false
    @Published var error: String?
    @Published var user: User?

    static let shared = AuthViewModel()

    init() {
        getAccount()
    }

    private func getAccount() {
        AppwriteService.shared.account.get() { result in
            DispatchQueue.main.async {
                switch result {
                case .failure(let err):
                    self.error = err.message
                    self.isLoggedIn = false
                case .success(let user):
                    self.user = user
                    self.isLoggedIn = true
                }

            }
        }
    }

    func create(name: String, email: String, password: String) {
        AppwriteService.shared.account.create(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)
            }
        }
    }

    func logout() {
        AppwriteService.shared.account.deleteSession(sessionId: "current") { result in
            DispatchQueue.main.async {
                switch result {
                case .failure(let err):
                    self.error = err.message
                case .success(_):
                    self.isLoggedIn = false
                    self.error = nil
                }
            }
        }
    }

    func loginAnonymous() {
        AppwriteService.shared.account.createAnonymousSession() { result in
            switch result {
            case .failure(let err):
                DispatchQueue.main.async {
                    self.error = err.message
                }
            case .success:
                self.getAccount()
            }
        }
    }

    public func login(email: String, password: String) {
        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()
            }
        }
    }
}
```

Here we are extending ObservableObject so that we can publish changes to our UI. We have two published var to keep track of errors and logged in state so that UI can subscribe to those and update accordingly. Next up we have methods for login, get account and logout. Here we are using the Appwrite's account service to perform those action. Logging in is as simple as calling a **createSession** method on account service object with email and password. Once the session is created calling the **get** method on account service will return the active user's details. Finally logging user out is simple by calling **deleteSession** method with session id parameter as `current` to delete the currently active session.

We need to update the **Appwrite_JobsApp** as the following.

```swift
import SwiftUI

@main
struct Appwrite_JobsApp: App {
    var body: some Scene {
        WindowGroup {
            MainView()
                .environmentObject(AuthViewModel.shared)
                .preferredColorScheme(.dark)
        }

    }
}
```

Here we are passing the instance of **AuthViewModel** as the environment object so that we can access it from all of our views.

## 👷‍♂️ Setting up Appwrite Project

Great work. Now the only thing left to do to get our authentication working is set up our Appwrite project. If you already have a project set up on your Appwrite console, you can skip to the next section. If not, and you have a freshly installed Appwrite following our installation guide, then you can signup to create a root account. You should now be logged in and see an empty list of projects. Click on the **Create Project** button and give your project a name. Finally, click **Create** to create your project. You should then be redirected to the project's dashboard.

![Project dashboard](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dgr6cb6n67ilnwx0aqrg.png)
 
Click Settings option to access the project settings. There on the right sidebar, you should find your endpoint and your project id. Update `YOUR_ENDPOINT` and `YOUR_PROJECT_ID` in the **AppwriteService** class with the values you obtain from project settings page. Finally you need to add a platform. Back to project home, scroll down and click on the **Add Platform** button and select `New iOS App` option. In the dialog box that appears, enter the easy to recognizable name for your platform and then the bundle id. You can find the bundle id for your project in XCode.

![XCode bundle ID](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8ll5kcgvyepq5adnmq5j.png)
 
## 💾 Setting up Database

Now that we have set up our project, time to set up the database, a jobs collection for saving the list of jobs. In the Appwrite console on left sidebar, click on **Database**. In the database page, you can click **Add Collection** to create a new collection. In the dialog box that appears, enter the name of your collection and click **Create**. This will redirect you to the newly created collection's page. We now need to add attributes to our collection.

On the collection settings page, tap on add and enter Label **Title**, key **title** and rule type **Text** and click on **create** button.

![Create title rule](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ta7sy5rr1n0evwasbb4v.png)
 
Once created, you can expand the rule under rules section and make it required. Similarly add the following rules

- Location
  - label : Location
  - key: location
  - rule type: Text
  - required: true 
- Link
  - label : Link
  - key: link
  - rule type: url
  - required: true 
- Company
  - label : Company
  - key: company
  - rule type: text
  - required: true

Set the read and write permission as the following.

![Read write permission](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2wlkmt3qfwtikcvl25ny.png)
 
## 🔬 Let's Test Authentication

Now that we have set up Appwrite project as well as our iOS project. It's time to build and test that authentication is working. Open your iOS project in XCode and hit the play icon in the top left to start building the project. Once built it will run inside the Simulator that is selected and if successful it should look like the following.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ejs9eyjudfo11jgrivzk.png) 
 
Tap **Anonymous Login**, you should be able to login and see the Home Page.

## 👮‍♀️ Listing Jobs

Time for listing jobs. We will start by creating **Job** model and update with the following code.

```swift
import Foundation

class Job: Identifiable {
    public let id: String
    public let title: String
    public let link: String
    public let logo: String
    public let company: String
    public let description: String
    public let location: String

    init(
        id: String,
        title: String,
        link: String,
        logo: String,
        company: String,
        description: String,
        location: String
    ) {
        self.id = id
        self.title = title
        self.link = link
        self.logo = logo
        self.company = company
        self.description = description
        self.location = location
    }

    public static func from(map: [String: Any]) -> Job {
        return Job(
            id: map["$id"] as! String,
            title: map["title"] as! String,
            link: map["link"] as! String,
            logo: map["logo"] as! String,
            company: map["company"] as! String,
            description: map["description"] as! String,
            location: map["location"] as! String
        )
    }

    public func toMap() -> [String: Any] {
        return [
            "title": title as Any,
            "link": link as Any,
            "logo": logo as Any,
            "company": company as Any,
            "description": description as Any,
            "location": location as Any
        ]
    }
}
```

Secondly, create the **JobItemView** and update with the following code.

```swift
import SwiftUI
import Kingfisher

struct JobItemView: View {
    let job: Job

    init(_ job: Job) {
        self.job = job
    }

    var body: some View {
        ZStack {
            RoundedRectangle(cornerRadius: 16, style: .continuous)
                .fill(.blue.opacity(0.2))

            VStack (alignment: .leading) {
                ZStack(alignment: .center) {
                    RoundedRectangle(cornerRadius: 14, style: .continuous)
                        .fill(.gray.opacity(0.5))

                    KFImage.url(URL(string: job.logo))
                        .resizable()
                        .scaledToFit()
                        .frame(height: 50)
                }
                .frame(width: 86, height: 82)

                Text(job.title)
                    .font(.largeTitle)
                    .padding(.top, 24)

                HStack (spacing: 20) {
                    Text(job.company)
                        .fontWeight(.semibold)
                    Text(job.location)
                        .fontWeight(.semibold)
                }
                .padding(.bottom, 24)

                Text(job.description)
            }
            .padding(.all, 26)
        }
    }
}

struct JobItemView_Previews: PreviewProvider {
    static var previews: some View {
        JobItemView(
            Job(
                id: "1",
                title: "Swift Developer",
                link: "https://appwrite.io",
                logo: "https://demo.appwrite.io/v1/storage/files/61667e8e6cb16/preview?project=615d75f94461f",
                company: "Google",
                description: "Swift Developer",
                location: "Tel Aviv"
            )
        )
        .preferredColorScheme(.dark)
    }
}
```

This will create a simple job item card view.

Let us also create the **JobsViewModel** file and update with the following code.

```swift
import Foundation

class JobsViewModel: ObservableObject {
    @Published var jobs: [Job] = []

    init() {
        getJobs()
    }

    func getJobs() {
        AppwriteService.shared.database.listDocuments(collectionId: "615ec687829fa") {
            result in
            DispatchQueue.main.async {
                switch result {
                case .failure(let err):
                    print(err.message)
                case .success(let docList):
                    let convert: ([String: Any]) -> Job = { dict in
                        return Job.from(map: dict)
                    }
                    self.jobs = docList.convertTo(fromJson: convert)
                }
            }
        }
    }
}
```

Here again we are creating an observable object so that we can get and display the list of jobs in our UI. To get the list of jobs we are calling listDocuments and passing the collection id of the collection we created. If successful we update the list of job by converting the json into our job model.

Finally let's update the **HomeView** with the following code.

```swift
import SwiftUI

struct HomeView: View {
    @EnvironmentObject var authVM: AuthViewModel
    @ObservedObject var jobsVM: JobsViewModel = JobsViewModel()

    var body: some View {
        VStack {
            HStack {
                Text("Appwrite Jobs")
                    .font(.title)
                    .fontWeight(.bold)
                Spacer()
                Text("Logout")
                    .onTapGesture {
                        authVM.logout()
                    }
            }
            .padding(.top, 40)
            .padding(.horizontal, 36)

            ScrollView {
                HStack {
                    Text("Find your dream\njobs")
                        .font(.largeTitle)
                    Spacer()
                }
                .padding(.vertical, 30)
                .padding(.horizontal, 36)

                ForEach(jobsVM.jobs) { job in
                    JobItemView(job)
                        .padding(.vertical, 12)
                        .padding(.horizontal, 36)
                }
            }
        }
    }
}

struct HomeView_Previews: PreviewProvider {
    static var previews: some View {
        HomeView()
            .preferredColorScheme(.dark)
    }
}
```

Now if you run and login you should see the list of jobs you have added in your collection. Mine looks as the following.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cnqqal971i6y41152y3w.png)
  
We've built a complete app that interacts with Appwrite's account and database APIs with our [SDK for Apple](https://github.com/appwrite/sdk-for-apple) and SwiftUI, which you can find over at our [GitHub Repo](https://github.com/appwrite/demo-job-portal-with-apple).

## ✨️ Credits

Hope you enjoyed this article! We can't wait to see what you will build. If you get stuck anywhere, feel free to reach out to us on our [friendly support channel](https://appwrite.io/discord) run by humans 👩‍💻.

Here are some handy links for more information:

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