SECRET OF CSS

Share Swift Code Between Client and Vapor Server | by Riccardo Cipolleschi | Aug, 2022


Set up a project with Vapor sharing most code

0*rsEKEwH 835H 5rZ
Photo by Abdelrahman Sobhy on Unsplash

One aspect of Swift that I haven’t studied yet is server-side development. One of the most famous frameworks for it is Vapor. Vapor is written in Swift, and it comes with all the standard server-side APIs: routing, request handling, validation, and so on.

Vapor also provides additional packages to interact with common services like databases, caches, web sockets, and so on. Its structure and dependencies are defined using SPM. And it is thanks to SPM that you can have the server and the client in the same project, sharing the main application logic.

Today, I want to explore how to set up a project where server and client code sits together in the same Xcode project where they can share pieces of logic.

The first step for this project setup is to install Vapor. You can follow the instructions on the official website. Run this command to install a brew formula:

brew install vapor

Once installed, you can create a first app to explore how Vapor is structured. From the terminal, run these commands:

  1. vapor new MyFirstServer -n to create the first server.
  2. cd MyFirstServer.
  3. open Package.swift.

The last command opens Xcode, and the IDE starts solving the dependencies. This may take some time to complete. Once done, you will see a MyFirstServer target for MacOS in the top bar of Xcode:

1*0uwJEzLjL

Click on the play button to start the server. Once loaded, the server responds to localhost, using the port 8080. If you connect to http://127.0.0.1:8080/, it outputs the message It works!.

By default, the server already responds to another endpoint, hello: the full address is http://127.0.0.1:8080/hello and the output message is Hello, world!, of course.

Vapor structure

The main structure of a Vapor base app is composed by:

  • A Public folder used to serve static files like images, icons, and script. It has to be configured to work properly, as explained here.
  • An App folder, which contains a Controllers folder where you can add all your logic. You can use the App folder to configure other logic elements. The configure function, which sets up the routes, lives here.
  • A Run folder with a main.swift file. This is the entry point of the server application: it loads environment variables, and then it creates, configures, and starts the app.

Package.swift

The core structure of the server application is defined by the Package.swift file. The template file looks like this:

The important bits to notice here are:

  • The Vapor dependency, loaded from GitHub in the dependency property.
  • The App target, which depends from the Vapor product.
  • the executableTarget named Run which depends on the target App.

This structure tells you that everything but the executableTarget can be actually moved to another package and imported by the Run target.

By extracting it, you would be able to share models and logic between the server and the client.

To isolate the executableTarget from the rest of the logic, you need first to create a separate Swift package.

  1. At the same level of MyFirstServer folder, create a MyAppLogic folder.
  2. Copy the Package.swift file from MyFirstServer to MyAppLogic folder.
  3. Update the MyAppLogic/Package.swift by removing the executableTarget.
  4. Move the Sources/App folder from MyFirstServer to MyAppLogic.
  5. Move the Tests/AppTests folder from MyFirstServer to MyAppLogic.
  6. In the Package.swift, rename the App into Server in both the targets and the AppTests to ServerTests. You may also want to change the package name to MyAppLogic.
  7. Rename all the folders from App to Server.
  8. Open the ServerTests.swift and change the @testable import from using App to using Server.
  9. In the Package.swift file, add a products property to generate a static library named Server which depends on the Server target.
  10. Press ⌘+b and see Xcode building the package successfully.

After all these steps, the file tree for the MyAppLogic folder should look like this:

MyAppLogic
├── Package.resolved
├── Package.swift
├── Sources
│ └── Server
│ ├── Controllers
│ ├── configure.swift
│ └── routes.swift
└── Tests
└── ServerTests
└── ServerTests.swift

While the Package.swift should look like this:

Next, you have to update the MyFirstServer Package.swift to consume the newly created package:

  1. Open the MyFirstServer/Package.swift file and remove the dependencies from Vapor and all the targets but the executableTarget.
  2. Add a dependency to the MyAppLogic package, using the .package(name:path:) option.
  3. Update the executableTarget dependency to consume the Server product from the MyAppLogic package.
  4. Update the main.swift file to import Server instead of import App.
  5. Run the server to make sure that everything works again.

The new Package.swift file for the server should now look like this:

At this point, you can’t keep both the MyAppLogic project and the MyFirstServer project open at the same time. Xcode is not able to read a package if it is open in another instance of Xcode.

When running the server, it could happen that you get a failure for some symbol missing. If that happens, try to open the MyAppLogic project and build the Server library. Now you should have the binaries ready for the MyFirstServer app to consume.

Finally, you can put everything together in a single project by creating a SwiftUI app. Start by creating a new project from Xcode and let’s call it MyFirstApp.

Once created, right-click on the app and choose the Add Packages option. From the dialog that appears, click the Add Local... button and select the MyAppLogic package. Repeat this operation to also add the MyFirstServer package.

1*H

This step puts all the code in the same project. The folder structure should now look like this:

1*fgySuoEuYH4D2OYIuN0Byw

Now, you can modify any Swift file for the client, the server, or the logic.

Using the Logic package in the app

To consume the MyAppLogic package in the app, you have to set up another library:

  1. Update the MyAppLogic/Package.swift to expose another library. Let’s call it Client.
  2. Add the .iOS platforms among the ones supported by the package.
  3. Create a MyAppLogic/Sources/Client folder.
  4. Move the ContentView from the MyFirstApp into it.

The final version of the MyAppLogic/Package.swift should now look like this:

Running the app

Thanks to this setup, you can now run the Server and the Client at the same time. In the top Xcode’s bar, the MyFirstApp target is selected and you can run it against some iOS simulator. Clicking on play may fail because, after the previous step, you moved the ContentView file from the SwiftUI app to the MyAppLogic package.

To make it build again, you can open the MyFirstAppApp.swift file and add the import Client statement on top.

Xcode should tell you that there is no such module as Client. That’s because you need to link the library to the app:

  1. Click on the project in the Project Navigator.
  2. Click on the General tab.
  3. Scroll down until you reach the Frameworks, Libraries, and Embedded Content section.
  4. Click on the + button and select the Client library from the MyAppLogic package.

Another trick that can save some time is to ensure that the Client library is recompiled when you build your app. Without this step, it could happen that you run the app without building the library and Xcode runs an outdated version of it, thus not showing you your recent changes.

This can be done by working on the Schemes:

  1. Click on MyFirstApp in the Xcode top bar.
  2. Select the Edit Scheme… option.
  3. In the build tab, click on the small + at the bottom.
  4. Add the Client library to it.

Now, every time you run the SwiftUI app, Xcode will also check whether some files in the Client library have changed and, eventually, will rebuild them.

Running the server

Currently, there is no way to run the server. That’s because there is no scheme for the server yet and you need to create one.

  1. Click on MyFirstApp in the top bar.
  2. Select the Manage Schemes… option.
  3. Click on the + button at the bottom.
  4. Open the Target, scroll down, and select the Run target.
  5. Call it MyFirstServer as you used to have before.
1*XjqhH3C0pG3pPayw4LYcdQ
1*Q2rli7aRxLn3emHj KFLwg

Now, you can edit the scheme and make sure that it also builds the Server library when you build the server app. The process is the same one you followed for the app:

  1. Click on MyFirstServer in the Xcode top bar
  2. Select the Edit Scheme… option.
  3. In the build tab, click on the small + at the bottom.
  4. Add the Server library to it.

When running the server, make sure that the executable target is My Mac, not any other flavour like My Mac (Designed for iPad). The server won’t build correctly in that case.

Finally, you can run the server, switch the desired target in the top Xcode bar and run the client app, at the same time.

You now have a setup where all your code stays in the same XCode’s project. You can modify any piece of any package at your will: so it’s time to share some code between the two apps.

Set up the package

Sharing code between Client and Server has several advantages. The most obvious is code reuse, of course. Secondly, it eliminates problems related to the data model going out of sync because the server updated them, but the client was left behind.

SPM can let you share code very easily, creating a new target used as a dependency by both apps.

  1. Create a new target in the MyAppLogic package, and call it Logic.
  2. Make both the Client and the Server target depend on it.
  3. Create a new folder MyAppLogic/Logic to add the shared code.

Remember to add the import Logic statement in the files that need that shared code.

The final Package.swift looks like this:

Notice line 31, where the Logic package is defined, and lines 22 and 32 where there are new dependencies in both the Client and the Server targets.

Data model

The simplest shareable thing between a client and a server are the data models. For example, you can create a Profile model that looks like this:

This Profile struct resides in the Logic target. It contains a username, name, surname, and birthdate of a user. It has a public init and a helper property to convert it to Json.

In this modular setup, visibility modifiers are important: if you don’t mark the init as public, you won’t be able to create a new structure in the Server package.

The server

Once the model is defined, you can use it in both the Server and the Client.

To use it in the Server library, you can update the routes.swift file to respond to another endpoint: profile. The code could look like this:

The snippet imports the Logic package in the first line to be able to access the Profile data structure. Then, it adds an endpoint called profile which accepts a path parameter. Vapor allows you to associate path parameters with labels you can use to retrieve their values.

The logic validates the requests, returning some error codes in case of failure. If everything is right, it creates the Profile structure and it returns it to the caller.

This is an example to quickly make the Client and the Server interact. The profile logic should be contained in a ProfileController to isolate the endpoint code from the route configuration.

As the last step, you can use the same struct in the Client. The process is similar, to the Server one. You don’t have routes in the client app, but you could embed them in a SwiftUI view.

Open the ContentView and modify it as follows:

The view lets you input a username and search it in the backend. Once the search completes, it renders the data in a table-like structure, which uses a subview called DataRow to arrange key-value pairs horizontally.

The search() function uses async-await to connect with the backend and it parses the data using the Profile struct defined in the Logic package. Once decoded, the object is stored in a @State variable and its values are used to populate the view.

The following video show the client and server in action:

This is an example to quickly make the client and the server interact. The logic to communicate with the server should be placed in a proper network component, as well as the SwiftUI subview should live in its own file.

Today I tried to condense several ideas into a single article. To summarise them:

  • How to set up a new project with Vapor, describing its basic structure.
  • How to separate the Vapor executable from the code.
  • How to create a single project with the server, the logic, and the client.
  • How to share code between the server and the client.
  • How to run them together.

This setup brings various advantages other than the code reuse:

  • It pushes a better modularisation and code structure.
  • It isolates the logic in a single SPM package, keeping the executables one-liners.
  • It lets you keep the code in sync between client and server.
  • It should be easier to write unit and integration tests.

The code discussed in this article can be found in this repository.



News Credit

%d bloggers like this: