DEV Community

Cover image for Let's Build A Job Portal With iOS
Damodar Lohani for Appwrite

Posted on

Let's Build A Job Portal With iOS

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. 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

📝 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.
  2. Access to XCode 12 or newer. Find more about Xcode here

🛠️ 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

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

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

🔐 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

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

Update the code of the LoginView to the following.

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

This will create a simple login view as the following.

Image description

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

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

This will create a simple signup view as the following.

Image description

Also create HomeView where later we will display the list of jobs.

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

Also create MainView and update with the following code

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()
    }
}
Enter fullscreen mode Exit fullscreen mode

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

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

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

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

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

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

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.

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

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()
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

import SwiftUI

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

    }
}
Enter fullscreen mode Exit fullscreen mode

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

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

💾 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

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

🔬 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

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.

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
        ]
    }
}
Enter fullscreen mode Exit fullscreen mode

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

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

This will create a simple job item card view.

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

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)
                }
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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

We've built a complete app that interacts with Appwrite's account and database APIs with our SDK for Apple and SwiftUI, which you can find over at our GitHub Repo.

✨️ 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 run by humans 👩‍💻.

Here are some handy links for more information:

Discussion (0)