Sharing Swift Code Between iOS and Server Applications

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

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

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