UIKit Apprentice, Second Edition – Now Updated!

Learn iOS and Swift from scratch. Build four powerful apps—with support for iPad and Dark Mode. Publish apps to the App Store.

Home Server-Side Swift Tutorials

Sharing Swift Code Between iOS and Server Applications

In this tutorial, you’ll learn how share code between iOS and server applications.

Version

  • Swift 5, macOS 11, Xcode 12
Update note: Christian Weinberger updated this tutorial for Vapor 4 and to account for the availability of SwiftPM on iOS. Tim Condon wrote the original.

Using Swift on the server allows you to use the same language you know and love from iOS development. In this tutorial, you’ll learn how to share code between iOS and server applications — the holy grail of full-stack development!

You’ll create a shared library using Swift Package Manager (SwiftPM). Then, you’ll import the library into your iOS and Server-Side Swift projects.

This tutorial uses Vapor for the Server-Side Swift app, but the same principles apply to any Swift-based framework. In this tutorial, you’ll learn how to:

  • Extract common models into a shared package.
  • Integrate the shared package into an iOS application.
  • Integrate the shared package into a Vapor application.
Note: This tutorial assumes you’ve installed Docker and have some basic experience with it. See Docker on macOS: Getting Started to learn more about Docker and how to install it. Additionally, you should have some experience using Vapor to build web apps. See Getting Started with Server-Side Swift with Vapor 4 if you’re new to Vapor.

Getting Started

Start by clicking the Download Materials button at the top or bottom of this tutorial.

The starter project contains two applications:

  • til-vapor-backend: The Vapor project for the API.
  • til-ios-app: The iOS app to consume the API.

The projects for this tutorial are based on the TIL app from the Server-Side Swift with Vapor book. You can create acronyms in the TIL app so you can remember their meaning. There are three relevant models in the project:

  • Acronym
  • User
  • Category

Why Share Code?

Sharing code between your client and server reduces duplication and means you only have to make changes in one place. For example, when you fix a bug in business logic or add a new property to a model, it’s fixed in both apps.

Sharing models is especially beneficial with Swift. If you use the same models on iOS and the server, you don’t need to worry about how the apps send the models in requests. You can send and receive them with JSON, Protobuf or even XML. As long as you use the same encoders and decoders on both ends, thanks to Codable, they’ll work.

Exploring the Vapor Project

Navigate to til-vapor-backend within starter package. Double-click Package.swift to open the Vapor app in Xcode.

While you wait for the SwiftPM to resolve dependencies, check out the existing project in Sources/App:

Exploring the Vapor project

Pay particular attention to:

  1. Controllers: This directory contains all API controllers to get or create acronyms, categories and users.
  2. Middlewares: This contains a custom middleware, TILErrorMiddleware, that encodes the custom TILError to a JSON response.
  3. Migrations: This has migrations for Acronym, Category and User as well as a migration to seed the database with initial data.
  4. Models: This contains various models used in the project. They’re grouped in subdirectories for API models, database entities and errors.
  5. Utilities: This has a convenience extension to Request that validates if a request has a specific parameter and throws a custom TILError otherwise. Endpoints.swift configures all endpoints used in the project, which you’ll learn more about later.
  6. configure.swift / routes.swift: configure.swift contains anything needed to run the project. For example, middleware and database configuration, as well as the registration of the migrations. routes.swift registers all controllers as RouteCollection.

Running the Vapor Project

Now that you’re a little more familiar with what the project contains, it’s time to dive in!

Spinning up the Database

Before you can run the Vapor project, you have to spin up a PostgreSQL database. This is already defined in the docker-compose file accompanying the Vapor project.

Go into Terminal and navigate to til-vapor-backend within the starter package. The root folder holds docker-compose, which contains a couple of images. For now, you only need the database (db) image.

In Terminal, run:

docker-compose up --build db

This builds db and spins up the PostgreSQL database with credentials that match those provided in .env:

Running docker-compose to spin up the database

Running the Migrations

First, run the migrations to create the database tables and populate your initial data. In Xcode, navigate to Product ▸ Schemes ▸ Edit Scheme in the menu bar. Select Run ▸ Arguments and add migrate -y in Arguments Passed On Launch.

Adding a launch argument to run migrations

This tells Vapor to run the migrations when you run the project. By passing the -y flag, you’re skipping interactive mode and auto-accept running the migrations.

Note: Before you can run the Vapor project, you have to configure the working directory. Otherwise, your back end won’t be able to locate the .env file. Do this by going to Product ▸ Schemes ▸ Edit Scheme in the menu bar, navigating to Run ▸ Options and checking Use custom working directory. Select the project folder here — for example, {path_to_your_projects}/til-vapor-backend.

Now, build and run the project to run the migrations. Switch to the console to see the output:

The result of the migrations

To run the actual project, head back to the Arguments Passed On Launch section and uncheck migrate -y.

Removing a launch argument to run migrations

Build and run. In the console, you’ll see that the server has started:

[ NOTICE ] Server starting on http://127.0.0.1:8080

Testing the API Using a REST Client

Now, switch to the REST client of your choice. This tutorial uses Paw, but alternatives such as Insomnia, Postman or even curl work fine as well.

Note: If you’re using Paw, there’s a project file in the root of the sample project that already contains all endpoints preconfigured for you: rw-code-sharing-ios-vapor.paw.

Call GET http://localhost:8080/api/users to verify that your project runs fine and the database contains the test data:

Testing your API with PAW REST Client

The server should respond with four users. Now, move on to the iOS app.

Exploring the iOS Project

Navigate to til-ios-app within the starter package and open the iOS project in Xcode by double-clicking TILiOS.xcodeproj.

Have a look at the project:

Exploring the iOS project

  1. Models: This directory contains all the API models: Acronym, Category and User.
  2. Main.storyboard: This contains the iOS app’s storyboard with all its screens.
  3. ViewControllers: All ViewControllers that are used to present or create new entries are in this directory.
  4. TILAPI.swift: This file contains the actual implementation of the HTTP communication with your Vapor app.

Running the iOS Project

To follow this tutorial, you should use the simulator instead of a physical iPhone. Otherwise, you’ll need to connect to your local Vapor server with your physical iOS device.

So now, select an iPhone simulator of your choice and run the project.

Running the app on iOS

The app has three sections in the tab bar that replicate the API controllers from the Vapor app: Acronyms, Users and Categories.

Identifying Sharable Components

Before you set up the projects to use a shared package, think about which components are worth sharing between the Vapor and iOS projects. As the iOS app communicates with the Vapor app to consume its API, this tutorial focuses on sharing the API models:

  • API models are the most obvious candidates. This is because Vapor uses them to decode models into JSON data, and iOS uses them to decode JSON data into models.

Other interesting parts you can share between iOS and Vapor that are not covered in this tutorial include:

  • API endpoints: Sharing them between the projects reduces the probability of spelling mistakes.
  • Errors: Sharing errors and error cases between iOS and Vapor makes error handling easier and also gives you type-safe cases.
  • Validations: Sharing validations of forms/models with the iOS app can be useful. It allows the app to present meaningful information about the user input before sending the data to the back end.
  • Business logic: Imagine your app has offline functionality. You could share parts of your business logic with the iOS app to enable an offline experience without rewriting the business logic for iOS.

Creating a Shared Package in Vapor

Open a new Terminal window — but don’t close the PostgreSQL instance — and navigate to the starter directory containing the two apps. You’ll create a new library to share with both. In Terminal, enter the following command:

#1
mkdir til-core
cd til-core
#2
swift package init --name TILCore

Here’s what the commands do:

  1. Make a new directory for the shared library and navigate into it.
  2. Create a new package using SwiftPM.

The output in Terminal shows you the new files created by SwiftPM. Run the following commands to create the files needed for the shared API models:

#1
mkdir Sources/TILCore/APIModels
#2
touch Sources/TILCore/APIModels/AcronymAPIModel.swift
touch Sources/TILCore/APIModels/UserAPIModel.swift
touch Sources/TILCore/APIModels/CategoryAPIModel.swift
#3
rm Sources/TILCore/TILCore.swift

Here’s what these new commands do:

  1. Create a new directory to contain the shared models.
  2. Make the files for the shared models.
  3. Remove the template file generated by SwiftPM.

Open the library project. In Terminal, enter

open Package.swift

This opens the package in Xcode. Since you deleted the TILCore.swift template file, you have to remove the example test that relies on it. Open TILCoreTests.swift and remove the line that says: XCTAssertEqual(TILCore().text, "Hello, World!").

Next, open Package.swift and rename the package name in line 7 from TILCore to til-core:

// ...
let package = Package(
    name: "til-core",
// ...

Then, build the TILCore project. It should succeed without errors.

Migrating UserAPIModel

Head back to tilbackend in Xcode and open UserAPIModel.swift. You’ll see:

import Vapor // #1

// #2
public struct UserAPIModel: Content {
  public let id: UUID
  public let name: String
  public let username: String

  // #3
  public init(id: UUID, name: String, username: String) {
    self.id = id
    self.name = name
    self.username = username
  }

  // #4
  init(user: User) throws {
    try self.init(
      id: user.requireID(),
      name: user.name,
      username: user.username
    )
  }
}

// #5
extension UserAPIModel {
  public struct Create: Codable {
    public let name: String
    public let username: String

    public init(name: String, username: String) {
      self.name = name
      self.username = username
    }

    // #6
    func makeUser() -> User {
      User(name: name, username: username)
    }
  }
}

Here’s what the file does:

  1. Since the API model is using Vapor’s Content protocol, it depends on the Vapor framework. For the shared package, you need to remove any dependencies specific to Vapor or iOS.
  2. This is the definition of the API model with a conformance to Content. Vapor requires this conformance so it can use automatic encoding to Response as needed by the framework.
  3. This is the initializer. UserAPIModel has an id, a name and a username.
  4. This is a convenience initializer to create UserAPIModel from User.
  5. This extension has a struct with all data required to create a new User via the API. It’s a good practice to have a separate model for this, as some fields aren’t provided when creating a User. For example, id is only known after the model has been created and stored in the database.
  6. A convenience method to create a User entity from UserAPIModel.Create.

Now, implement a version of UserAPIModel for your TILCore library that you can share with your Vapor and iOS apps:

  • First, copy and paste the contents of Vapor’s UserAPIModel.swift into your UserAPIModel.swift file in the TILCore library project.
  • Then, get rid of the Vapor dependency. Remove import Vapor and replace it with import Foundation.
  • Next, replace the conformance to Content with Codable. This allows you to decode and encode the object without the Vapor-specific protocol conformance.
  • Remove the convenience initializer init(user:) (see #4 in the code above) that’s only needed by Vapor and requires a User entity.
  • Last but not least, delete makeUser() within UserAPIModel.Create.

Your UserAPIModel.swift in the TILCore project will look like this now:

import Foundation

public struct UserAPIModel: Codable {
  public let id: UUID
  public let name: String
  public let username: String

  public init(id: UUID, name: String, username: String) {
    self.id = id
    self.name = name
    self.username = username
  }
}

extension UserAPIModel {
  public struct Create: Codable {
    public let name: String
    public let username: String

    public init(name: String, username: String) {
      self.name = name
      self.username = username
    }
  }
}

Migrating CategoryAPIModel

Next, repeat the steps with Vapor’s CategoryAPIModel:

  • First, copy and paste the contents of Vapor’s CategoryAPIModel.swift into your CategoryAPIModel.swift file in the TILCore library project.
  • Replace import Vapor with import Foundation.
  • Next, replace the conformance to Content with Codable.
  • Remove the convenience initializer init(category:) that’s only needed by Vapor and requires a Category entity.
  • Finally, delete makeCategory() within CategoryAPIModel.Create.

This is how CategoryAPIModel.swift will look now:

import Foundation

public struct CategoryAPIModel: Codable {
  public let id: UUID
  public let name: String

  public init(id: UUID, name: String) {
    self.id = id
    self.name = name
  }
}

extension CategoryAPIModel {
  public struct Create: Codable {
    public let name: String

    public init(name: String) {
      self.name = name
    }
  }
}

Migrating AcronymAPIModel

Now, you’re migrating the last API model, AcronymAPIModel:

  • First, copy and paste the contents of Vapor’s AcronymAPIModel.swift into your AcronymAPIModel.swift file in the TILCore library project.
  • Replace import Vapor with import Foundation.
  • Next, replace the conformance to Content with Codable.
  • Remove the convenience initializer init(acronym:) that’s only needed by Vapor and requires an Acronym entity.
  • Finally, delete makeAcronym() within AcronymAPIModel.Create.

The AcronymAPIModel.swift in TILCore will look like this now:

import Foundation

public struct AcronymAPIModel: Codable {
  public let id: UUID
  public let short: String
  public let long: String
  public let user: UserAPIModel
  public let categories: [CategoryAPIModel]

  public init(
    id: UUID,
    short: String,
    long: String,
    user: UserAPIModel,
    categories: [CategoryAPIModel]
  ) {
    self.id = id
    self.short = short
    self.long = long
    self.user = user
    self.categories = categories
  }
}

extension AcronymAPIModel {
  public struct Create: Codable {
    public let short: String
    public let long: String
    public let userID: UUID

    public init(short: String, long: String, userID: UUID) {
      self.short = short
      self.long = long
      self.userID = userID
    }
  }
}

Build the TILCore project. It should succeed without errors.

Integrating With Your Vapor App

Now that your TILCore project is up and running, it’s time to integrate!

Setting Up a Local Git Repository for Your Library

At the time of writing, Xcode does not support having two projects open that depend on a local Swift package — specified using a path — at the same time. To work around this limitation, create a local git repository for your TILCore project and add it as a git dependency to your iOS and Vapor apps instead.

In Terminal, switch to your til-core directory within the starter package and run these commands:

#1
git init
#2
git add .
git commit -m "Initial commit"
git branch -M main
#3
pwd

This is what it does:

  1. Initializes a new git repository for your til-core directory.
  2. Adds all changed files to the Git index and commits them into your local repository. It then changes the branch to main.
  3. Prints out the full path of the directory, in this case it’s /Users/cweinberger/dev/cweinberger/rw-code-sharing-ios-vapor/til-core.
Note: Since you’re integrating the library as a git dependency, you have to git add and git commit any updates you do to TILCore before they’re visible in your Vapor and iOS app.

Adding the Library to Your Vapor App

Head over to your Vapor project and open Package.swift. Currently, there are three dependencies: Vapor, Fluent and Fluent PostgreSQL Driver. Add a new one below .package(url: "https://github.com/vapor/fluent-postgres-driver.git", from: "2.0.0"),:

.package(url: "file:///Users/cweinberger/dev/cweinberger/rw-code-sharing-ios-vapor/til-core", .branch("main"))

This adds the dependency from the local TILCore git repository. The URL is the printed output of pwd that you ran, recently prefixed with file://.

Next, add TILCore as a dependency to your App target. Below .product(name: "Vapor", package: "vapor"), add:

.product(name: "TILCore", package: "til-core")

If Xcode doesn’t update the dependencies, you can trigger this manually. In the Xcode menu bar, select File ▸ Swift Packages ▸ Update to Latest Package Versions.

Configuring the API Models

To use the API models in Vapor, they must conform to Vapor-specific protocols. You also want to keep the Vapor-specific implementation you removed from the shared package.

In your Vapor project, create a new folder, APIModels+Vapor, under Sources/App/Models and add three new Swift files: AcronymAPIModel+Acronym.swift, CategoryAPIModel+Acronym.swift and UserAPIModel+Acronym.swift.

Open AcronymAPIModel+Acronym.swift and insert the following:

// #1
import TILCore
import Vapor

// #2
extension AcronymAPIModel: Content {}

// #3
extension AcronymAPIModel {
  init(_ acronym: Acronym) throws {
    try self.init(
      id: acronym.requireID(),
      short: acronym.short,
      long: acronym.long,
      user: UserAPIModel(user: acronym.user),
      categories: acronym.categories.map { try CategoryAPIModel(category: $0) }
    )
  }
}

// #4
extension AcronymAPIModel.Create {
  func makeAcronym() -> Acronym {
    Acronym(short: short, long: long, userID: userID)
  }
}

Here’s what the code does:

  1. Imports TILCore to have access to the shared models and Vapor to add Vapor-specific implementation.
  2. Conforms to Vapor’s Content protocol.
  3. Brings back the initializer that initializes AcronymAPIModel from Acronym.
  4. Brings back the convenience method that creates Acronym from AcronymAPIModel.Create.

Now, repeat the steps for the other API models.

Open CategoryAPIModel+Category.swift and insert:

import TILCore
import Vapor

extension CategoryAPIModel: Content {}

extension CategoryAPIModel {
  init(category: Category) throws {
    try self.init(
      id: category.requireID(),
      name: category.name
    )
  }
}

extension CategoryAPIModel.Create {
  func makeCategory() -> Category {
    Category(name: name)
  }
}

Open UserAPIModel+User.swift and insert:

import TILCore
import Vapor

extension UserAPIModel: Content {}

extension UserAPIModel {
  init(user: User) throws {
    try self.init(
      id: user.requireID(),
      name: user.name,
      username: user.username
    )
  }
}

extension UserAPIModel.Create {
  func makeUser() -> User {
    User(name: name, username: username)
  }
}

Next, remove the APIModels folder in your Vapor project by right-clicking the folder and selecting Delete.

Build the Vapor project now and you’ll find a lot of errors similar to:
/Sources/App/Controllers/AcronymsController.swift:45:65: Cannot find type ‘AcronymAPIModel’ in scope.

Puppy monster with surprised face

Your API models are no longer part of the Vapor project, they are part of the TILCore package. You have to import TILCore anywhere you use these models.

Add import TILCore to the three controllers: AcronymsController.swift, CategoriesController.swift and UserController.swift.

Build and run the project and notice that all errors are gone.

Puppy monster with happy face

Great! Your Vapor project now uses the API models from your TILCore library! It also extends them with the required Vapor-specific functionalities. :]

Integrating With Your iOS App

Now you’ve integrated TILCore with your Vapor app, it’s time to do the same with your iOS app!

Adding the Library to Your iOS App

Head back to your iOS app project and select the project file. In the main window, select Project ▸ TILiOS, navigate to the Swift Packages tab and click the + button.

Enter the same repository URL that you’ve used for the Vapor app: file:///Users/cweinberger/dev/cweinberger/rw-code-sharing-ios-vapor/til-core, in this case.

Click Next and make sure that you select the Branch radio button under Rules. The main branch should show up. Select Next and double-check that TILiOS is selected as the target. Complete the process by clicking Finish.

Add TILCore package to the iOS project

You’ll see the til-core package in your Swift Package Dependencies now.

Using the Shared API Models

Next, delete the Models folder in your iOS project, as you want to use the ones from the TILCore package. Build the project and you’ll see a lot of errors similar to the ones you got on the Vapor project earlier: TILiOS/ViewControllers/CreateUserTableViewController.swift:65:16: Cannot find ‘UserAPIModel’ in scope.

Add import TILCore to all the ViewControllers and to TILAPI.swift.

Build and run the Vapor project, then build and run the iOS app.

Great! Your iOS app works as expected and shares the API models with your Vapor app. :]

Swift mascot badge

Cleaning Up

Stop your Vapor project to stop the Vapor server.

To stop the PostgreSQL database, head to the Terminal window where docker-compose is still running, and type Control-C.

To remove the images and containers created by docker-compose, in Terminal, in the same directory as your docker-compose file, run:

docker-compose down -v --rmi all --remove-orphans

Where to Go From Here?

You can download the completed project files by clicking the Download Materials button at the top or bottom of the tutorial.

In this article, you’ve learned how to create a library to share Swift code between server-side apps and iOS apps! You’ve built a shared library using SwiftPM. You’ve also learned how to integrate the library into an iOS app and a Server-Side Swift app.

Now that you have code shared between two different apps, try adding new properties to the models and see how easy it is to keep them working together.

If you’re looking for a challenge beyond the scope of this article, here are a couple things you can try:

  • Migrate Endpoints to the shared package, similar to what you did with the API models.
  • Share TILError from the Vapor project with iOS.

To learn more about Server-Side Swift and Vapor, check out:

We hope you enjoyed this tutorial. If you have any questions, comments or ideas on what else to share between iOS and Vapor, please join the forum discussion below!

Add a rating for this content

More like this

Contributors

Comments