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.
📝 Technical Requirement
In order to continue with this tutorial, you will need to have the following:
- 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.
- 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.
On the next screen, give your project a name, organization id and in interface select SwiftUI and language Swift, then click next.
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.
🔐 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.
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.
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)
}
}
This will create a simple login view as the following.
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)
}
}
This will create a simple signup view as the following.
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)
}
}
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()
}
}
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.
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
.
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.
Before we can continue, we first need to add the Appwrite Swift SDK as a dependency. To do that, go to File -> Add Packages.
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.
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.
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)
}
}
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()
}
}
}
}
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)
}
}
}
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.
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.
💾 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.
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.
🔬 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.
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
]
}
}
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)
}
}
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)
}
}
}
}
}
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)
}
}
Now if you run and login you should see the list of jobs you have added in your collection. Mine looks as the following.
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:
Top comments (3)
Great article! Lot of potential with appwrite! If you have to choose a specific database with appwrite.. which one would you choose?
What do you mean by specific database? Appwrite has it's own database as you can see in the tutorial we are storing and loading job entries via Appwrite's database. Appwrite's database is built on top of MariaDB and is the only supported DB til this version, however the upcoming database refactor will allow us to support multiple underlying databases from which we can choose one to use.
thanks for your quick reply! That's great that multiple databases integration is coming in the near future...