Advanced iOS Summer Bundle

3 brand-new books on SwiftUI, Combine and Catalyst — $99.99 for a limited time!

Sharing Swift Code Between iOS and Server Applications

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

5/5 5 Ratings

Version

  • Swift 5, macOS 10.14, Xcode 10

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 share code between iOS and server applications — the holy grail of full-stack development!

You’ll create a library that works with both Swift Package Manager and CocoaPods. This allows you to import the library into your iOS projects and your server-side Swift projects.

This tutorial uses Vapor for the server-side Swift app, but the same principles apply for Kitura.

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.

Getting Started

Start by clicking the Download Materials button at the top or bottom of this tutorial. The starter project contains two applications:

  • TILApp: The Vapor app for the API
  • TILiOS: 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. The TIL app allows you to create acronyms so you can remember their meaning. There are three models in the project:

  • Acronym
  • User
  • TILCategory

The library you create will share these models across the different apps.

Creating a Shared Package

Open Terminal and navigate to the starter directory containing the two apps. You’re going to create a new library to share with both. In Terminal, enter the following commands:

# 1
mkdir TILCore
cd TILCore
# 2
swift package init

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 Swift Package Manager (SPM).

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

# 1
mkdir Sources/TILCore/Models
# 2
touch Sources/TILCore/Models/Acronym.swift
touch Sources/TILCore/Models/User.swift
touch Sources/TILCore/Models/TILCategory.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 SPM.

Finally, in Terminal run the following command:

swift package generate-xcodeproj

This command uses SPM to generate an Xcode project for you to work in.

Setting up the Shared Models

Open the generated project in Xcode and open Sources/TILCore/Models/User.swift. Enter the following code:

import Foundation

public final class User: Codable {
  public var id: UUID?
  public var name: String
  public var username: String

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

The user has three properties:

  • An optional UUID ID.
  • A name as a String.
  • A username as a String.

Next, open Acronym.swift and enter the following:

import Foundation

public final class Acronym: Codable {
  public var id: Int?
  public var short: String
  public var long: String
  public var userID: UUID

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

The acronym has four properties:

  • An optional Int ID.
  • A String property called short, which holds the acronym itself.
  • A String property called long, which holds the meaning of the acronym.
  • The ID of the user that created the acronym.

Finally, open TILCategory.swift and insert the following:

import Foundation

public final class TILCategory: Codable {
  public var id: Int?
  public var name: String

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

The category contains an optional Int for the ID and the name of the category as a String. Set the targeted device to My Mac and build the project in Xcode to make sure it compiles.

Congratulations! You’ve created your library!

Note: For the models to be accessible from external projects they must be public. All models also conform to Codable.

Creating a Podspec

Since you created the library with SPM, it already works with other SPM projects. To use it with CocoaPods in an iOS project ,you must create a podspec. In the directory containing the Package.swift manifest, create a new file called TILCore.podspec. Open the file in a text editor and insert the following:

# 1
Pod::Spec.new do |s|
  # 2
  s.name = "TILCore"
  s.version = "0.0.1"
  s.summary = "TIL Core for code shared across platforms"
  s.description  = <<-DESC
    iOS Podspec for TILCore for sharing between iOS and Server
    DESC
  # 3
  s.homepage = "https://www.raywenderlich.com/server-side-swift"
  # 4
  s.license = "None"
  # 5
  s.author = "Tim Condon"
  # 6
  s.platform = :ios, "12.0"
  # 7
  s.source = { :git => "git@github.com:0xTim/SSSTILCodeSharing.git",
		           :tag => "#{s.version}" }
  # 8
  s.source_files = "Sources/TILCore/**/*.swift"
  # 9
  s.swift_version = '5.0'
end

Here’s what the podspec does:

  1. Specify a new CocoaPods podspec.
  2. Set the name, version, summary and description for the library.
  3. Set the homepage for the project. Use this so other people can find out more about the library.
  4. Specify the license type for the project.
  5. Set the author — replace this with your name! :]
  6. List the supported platforms for the project — in this case iOS 12.0 and above.
  7. Set the Git repo and tag format for the library. CocoaPods uses this to work out how to install the project. Note: the Git URL doesn’t matter for this tutorial as you’ll use a local cocoapod.
  8. Tell CocoaPods where to look for the source files. This must match SPM’s requirements since SPM dictates where the source lives.
  9. Set the Swift version for this project.

For this tutorial, you’ll use this library as a local pod, so you don’t need to worry about publishing the library. For a real project, check out Keegan’s awesome tutorial on creating a CocoaPod.

You’ve created your first cross-platform library. Congratulations! Now to use it! :]

Integrating With Your Server-side Swift App

Navigate to the TILApp directory in Terminal. Open Package.swift in Xcode or your favorite editor. The manifest currently has two dependencies: Fluent SQLite and Vapor. In the dependencies array, add a new one below .package(url: "https://github.com/vapor/fluent-sqlite.git", from: "3.0.0"),:

.package(path: "../TILCore")

This adds the dependency from the TILCore directory. This is a local dependency in SPM that allows you to test and develop your libraries quickly. Next, replace .target(name: "App", dependencies: ["FluentSQLite", "Vapor"]), with the following:

.target(name: "App", dependencies: ["FluentSQLite", "Vapor", "TILCore"]),

This adds the TILCore dependency to the App target. Finally, generate the Xcode project in Terminal:

swift package generate-xcodeproj

This downloads all the dependencies and links the TILCore library you created earlier.

Configuring the Models

To use the models in Vapor, they must conform to Vapor-specific protocols. Open the generated Xcode project and open Sources/App/Models/TILCore+Vapor.swift.

First, import your new library below import FluentSQLite:

import TILCore

Next, insert the following:

// 1
extension User: SQLiteUUIDModel {}
// 2
extension User: Migration {}
// 3
extension User: Content {}
// 4
extension User: Parameter {}

Here’s what the new code does:

  1. Conform User to SQLiteUUIDModel. This allows you to perform queries with Fluent on the User model in the database.
  2. Conform User to Migration. This allows Fluent to create the User table in the database.
  3. Make User conform to Content. Content is Vapor’s wrapper around Codable and allows you to send and receive Users as JSON.
  4. Conform User to Parameter. This allows you to do model-based routing with Vapor.

Next, add the following below your new code:

extension TILCategory: SQLiteModel {}
extension TILCategory: Migration {}
extension TILCategory: Content {}
extension TILCategory: Parameter {}

This does the same as the extensions you wrote for User. The only difference is that TILCategory conforms to SQLiteModel instead of SQLiteUUIDModel as the ID for TILCategory is an Int.

Next, below the extensions for TILCategory, add the following:

extension Acronym: SQLiteModel {}
extension Acronym: Migration {
  // 1
  public static func prepare(on connection: SQLiteConnection) -> Future<Void> {
    // 2
    return Database.create(self, on: connection) { builder in
      // 3
      try addProperties(to: builder)
      // 4
      builder.reference(from: \.userID, to: \User.id)
    }
  }
}
extension Acronym: Content {}
extension Acronym: Parameter {}

This conforms Acronym to the same protocols as User. However there’s a change for the conformance to Migration.

Here’s what the new code does:

  1. Implement prepare(on:) from Migration to override the default implementation.
  2. Create the Acronym table in the database.
  3. Add all the fields to the database using the default options.
  4. Set up a reference between Acronym‘s userID and ID from the User table.

Even though you defined your models in a separate package, you can still set up complex database tables with Fluent!

Note: For integrating shared models with Kitura and Swift-Kuery-ORM, conform your model to Model.

Next, open AcronymCategoryPivot.swift. Uncomment the code portion of this file. It contains a pivot model to manage the sibling relationship between the Acronym and Category models. The pivot is a database-specific model that you don’t need to share.

Finally, in TILCore+Vapor.swift add the following to the end of the file:

extension User {
  var acronyms: Children<User, Acronym> {
    return children(\Acronym.userID)
  }
}

extension TILCategory {
  var acronyms: Siblings<TILCategory, Acronym, AcronymCategoryPivot> {
    return siblings()
  }
}

extension Acronym {
  var user: Parent<Acronym, User> {
    return parent(\.userID)
  }

  var categories: Siblings<Acronym, TILCategory, AcronymCategoryPivot> {
    return siblings()
  }
}

This code adds computed properties to the different models. These properties use Fluent’s convenience helpers to make it easy to access and use the different relationships in your app.

Seeding the Database

You’re now ready to use the models from your shared library in the Vapor app. First, open Seeds.swift and uncomment the code. This file contains a number of migrations to add some data to the database. Next, open configure.swift. Below import Vapor add the following:

import TILCore

This imports the shared library so you can see your shared models. Next, below var migrations = MigrationConfig(), add the following:

migrations.add(model: User.self, database: .sqlite)
migrations.add(model: Acronym.self, database: .sqlite)
migrations.add(model: TILCategory.self, database: .sqlite)
migrations.add(model: AcronymCategoryPivot.self, database: .sqlite)
migrations.add(migration: DefaultUsers.self, database: .sqlite)
migrations.add(migration: DefaultAcronyms.self, database: .sqlite)
migrations.add(migration: DefaultCategories.self, database: .sqlite)

This adds a number of migrations to your app. First, it creates the tables for the different users. Finally, it uses the seed information to populate those tables.

Note: The main objective of this tutorial is to show how to share code between projects. The details of integrating the shared code with Vapor and Fluent are outside the scope of this tutorial but are covered extensively in other Ray Wenderlich tutorials and books.

Setting up the Categories Routes

Next, you need to set up some routes. Open Controllers/CategoriesController.swift. Right after import Vapor, import the shared library:

import TILCore

If Xcode shows you a No such module ‘Vapor’ error, ignore it. It will disappear when you have all the parts in place and build the app.

Next, below boot(router:), add the following:

// 1
func createHandler(_ req: Request, category: TILCategory) throws
  -> Future<TILCategory> {
    // 2
    return category.save(on: req)
}

// 3
func getAllHandler(_ req: Request) throws -> Future<[TILCategory]> {
  // 4
  return TILCategory.query(on: req).all()
}

// 5
func getHandler(_ req: Request) throws -> Future<TILCategory> {
  // 6
  return try req.parameters.next(TILCategory.self)
}

// 7
func getAcronymsHandler(_ req: Request) throws -> Future<[Acronym]> {
  // 8
  return try req.parameters.next(TILCategory.self)
	  .flatMap(to: [Acronym].self) { category in
      // 9
      try category.acronyms.query(on: req).all()
  }
}

This code defines a number of new route handlers. Here’s what they do:

  1. Define a route handler to create a category. The route handler takes a Request and the decoded TILCategory sent in the request. The route handler returns the category once it’s saved in the database.
  2. Save the category in the database.
  3. Define a route handler to get all the categories. The route handler takes a Request and returns an array of categories.
  4. Use Fluent to query the TILCategory table and return all the categories found.
  5. Define a route handler to get a single category. The route handler takes a Request and returns the requested category.
  6. Return the TILCategory from the request’s parameters. This reads the path, e.g. /api/categories/<CATEGORY_ID>, and returns the category with the ID provided.
  7. Define a route handler to get a category’s acronyms. The route handler takes a Request and returns the acronyms associated with that category.
  8. Get the TILCategory provided in the request’s parameters like getHandler(_:).
  9. Use the computed properties you defined above to get all the category’s acronyms.

Next, below let categoriesRoute = router.grouped("api", "categories"), add the following to register the routes so requests get passed to one of the methods you created:

// 1
categoriesRoute.post(TILCategory.self, use: createHandler)
// 2
categoriesRoute.get(use: getAllHandler)
// 3
categoriesRoute.get(TILCategory.parameter, use: getHandler)
// 4
categoriesRoute.get(TILCategory.parameter, "acronyms", use: getAcronymsHandler)

Here’s what the new code does:

  1. Route a POST request to /api/categories/ to createHandler(_:category:). Use the convenience function to decode the request’s body to TILCategory.
  2. Create a route to send a GET request to /api/categories/ to getAllHandler(_:).
  3. Route a GET request to /api/categories/<CATEGORY_ID> to getHandler(_:).
  4. Route a GET request to /api/categories/<CATEGORY_ID>/acronyms to getAcronymsHandler(_:).

Setting up the Other Controllers

First, open UsersController.swift and uncomment the code. Do the same with AcronymsController.swift. These contain the route handlers for users and acronyms similar to the routes you set up for categories. Finally, open routes.swift and uncomment the code in routes(_:). This sets your controllers up with the Vapor app.

Ensure you have the Run scheme selected and the My Mac device selected:

Scheme and target for TILApp

Build and run the app and you should see Server starting on http://localhost:8080 in the console:

TILApp running

Integrating With Your iOS App

With the API running, you can work on the iOS app! Keep the TILApp running and open TILiOS.xcodeproj in the TILiOS directory.

The iOS project already uses the models. They’re duplicated from the shared library. Build and run the iOS app and you’ll see the tables with seed data:

TIL iOS App with seed data

Close the Xcode project and create a new file named Podfile in the TILiOS directory. Open the file in an editor and insert the following:

# 1
platform :ios, '12.0'

# 2
target 'TILiOS' do
  # 3
  use_frameworks!
  # 4
  pod 'TILCore', :path => '../TILCore'
end

Here’s what the Podfile does:

  1. Set the platform for the workspace to iOS 12.0.
  2. Set up the TILiOS target.
  3. Tell CocoaPods to use frameworks instead of libraries.
  4. Integrate the CocoaPod you created earlier. This pulls it in as a local dependency so you don’t have to release it to try it out.

In Terminal, in the TILiOS directory, run the following command:

pod install

This creates a new workspace with the existing iOS project and pulls in the TILCore library. Open the generated workspace, TILiOS.xcworkspace. In the Project navigator, select the three models in the TILiOS/Models directory in the iOS project.

Warning: make sure you select the models from TILiOS and not TILCore!

Right-click and select Delete and Move to Trash to remove the models from the project. You won’t need them since you’ll use the models from TILCore!

Delete models

Time to fix the errors. First, open Utilities/AcronymRequest.swift. Below import Foundation, insert the following:

import TILCore

Next, import TILCore into every file in ViewControllers:

  • AcronymsTableViewController.swift
  • UsersTableViewController.swift
  • CategoriesTableViewController.swift
  • CreateCategoryTableViewController.swift
  • CreateUserTableViewController.swift
  • AcronymDetailTableViewController.swift
  • CreateAcronymTableViewController.swift
  • SelectUserTableViewController.swift
  • AddToCategoryTableViewController.swift

Build and run the app. You can still use the app and create acronyms using the same model code as the API!

Cool Swift Bird with Sunglasses

Where to Go From Here?

You’ve learned how to create a library to share Swift code between server-side apps and iOS apps! In this tutorial you’ve built a shared library that works with both Cocoapods and SPM. You’ve also learned how to integrate the library into an iOS application and a server-side Swift application.

In a real server-side Swift app, you may have complicated models and different public representations of your models. In this case, you should only share the public representations of the models — i.e., the code that both apps will use. Access all the project materials from this tutorial by clicking the Download Materials button at the top or bottom of the page.

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.

Average Rating

5/5

Add a rating for this content

5 ratings

Contributors

Comments