Kitura Stencil Tutorial: How to make Websites with Swift

Build a web-based frontend for your Kitura API using Stencil, in this server-side Swift tutorial! By David Okun.

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

Loops and Other Operations in Stencil

Just like control flow in most modern programming languages, Stencil has a way to handle looping through an array of objects that you may want to utilize. Using our previous example, say you render an array into your response like so:

let birthdays = [CurrentMonth(month: "September"), 
                 CurrentMonth(month: "January"),
                 CurrentMonth(month: "March")]
                 
try response.render("home.stencil", with: birthdays, 
  forKey: "birthdays")

This means you now have a collection of objects available to you in Stencil rather than just a singular object.

Now, you need to make use of one of Stencil’s built in template tags with a for loop. Your HTML might look like this:

<html>
  <body>
  {% for birthday in birthdays %}
    <li> Month: {{ birthday.month }} </li>
  {% endfor %}
  </body>
</html>

Notice the difference between the {% and the {{ delimiters. As you have already learned, anything inside {{ }} is going to represent as a string inside your rendered HTML document. However, if something is inside the {% %} delimiters, then this is going to apply to a tag that Stencil recognizes. Many Stencil tags require a delimited tag like endfor that “ends” the previous command.

The for loop is certainly an example of that, but you can also do similar with an if statement in Stencil like so:

{% if birthday.month %}
  <li> Month: {{ birthday.month }}</li>
{% else %}
  <li> No month listed. </li>
{% endif %}

Here’s a partial list of some of the built in tags that Stencil has available for you to use:

  • for
  • if
  • ifnot
  • now
  • filter
  • include
  • extends
  • block

If you want to check the documentation on how you can use any of these tags in your own website template, visit Kyle’s documentation website at https://stencil.fuller.li/en/latest/builtins.html.

Note: Of the tags you can use, the include tag is of particular interest, as you can pass a context through to another .stencil file in your original file by typing {% include "secondTemplate.stencil" %}. You won’t use it in this project, but some websites can become a bit cumbersome if you don’t split them up — this can be helpful!

With this theory behind you, it’s finally time to start updating your starter project!

Setting up PostgreSQL

In order to build the sample project, you’ll need to have PostgreSQL installed on your development system, which you can do using Homebrew.

Run the following command in Terminal to install PostgreSQL, if it’s not already installed:

brew install postgres

Then start the database server and create a database for the EmojiJournal app using these commands:

pg_ctl -D /usr/local/var/postgres start
initdb /usr/local/var/postgres
sudo -u postgres -i
createdb emojijournal

Note: You may also have some luck running PostgreSQL in a Docker container.

Adding Stencil to Your Project

Open up EmojiJournalServer/Package.swift in a text editor.

First, add the Stencil dependency at the bottom of your list of dependencies:

.package(url: 
"https://github.com/IBM-Swift/Kitura-StencilTemplateEngine.git", 
.upToNextMinor(from: "1.11.0")),

Make sure the previous line has a , at the end, or your Package.swift won’t compile.

Next, scroll down to the Application target, and in the list of dependencies this target has, add KituraStencil to the end of the array. It should look like this:

.target(name: "Application", dependencies: [ "Kitura", "CloudEnvironment", "SwiftMetrics", "Health", "KituraOpenAPI", "SwiftKueryPostgreSQL", "SwiftKueryORM", "CredentialsHTTP", "KituraStencil"]),

Save your file and navigate to the root directory of your project in Terminal.

This is a good time to remind you that the command swift package generate-xcodeproj command will resolve your dependency graph for you, and then generate an Xcode project for you. And remember that Swift Package Manager runs swift package update and swift build under the hood for you!

Run the command swift package generate-xcodeproj from the EmojiJournalServer folder and when it’s done, open EmojiJournalServer.xcodeproj. Build your project in Xcode, and make sure everything runs OK.

Web Client Routing

The starter project contains a router for JournalEntry objects and for UserAuth management. You’re going to add another route for managing connecting to your web client.

In Xcode, navigate to the Sources/Application/Routes folder. Create a new file, and name it WebClientRoutes.swift.

At the top of this file, import the following libraries:

import Foundation
import LoggerAPI
import KituraStencil
import Kitura
import SwiftKueryORM

Now, add a function that will help you register a route on your main Router object to handle the web client:

func initializeWebClientRoutes(app: App) {

  // 1
  app.router.setDefault(templateEngine: StencilTemplateEngine())
  
  // 2
  app.router.all(middleware: StaticFileServer(path: "./public"))
  
  // 3
  app.router.get("/client", handler: showClient)
  
  // 4
  Log.info("Web client routes created")
}

Look at each line of this function you’ve just added:

  1. Since you’ve added the Stencil dependency to your project, you need to tell Kitura that Stencil is going to be the format for templating your HTML. Yes — you do have a choice when it comes to other templating engines, but our team has chosen Stencil!
  2. Here, you need to tell Kitura that, when searching for static files to serve up (images, etc.) which directory to look in, and this tells Kitura to look in your aptly named public directory.
  3. Here, you are registering the /client route on your router, and you’ll handle this route with Stencil and Kitura shortly.
  4. Log, log, log your work!

Beneath this function, add the following function signature:

func showClient(request: RouterRequest, 
  response: RouterResponse, next: @escaping () -> Void) {

}

This way, your project can compile, and you’ve now specified a function to handle your route. From here on out, you’re going to write a bunch of Swift code that serves up what you will eventually shape in a Stencil file.

Start by declare the following object above your initializeWebClientRoutes function:

struct JournalEntryWeb: Codable {
  var id: String?
  var emoji: String
  var date: Date
  var displayDate: String
  var displayTime: String
  var backgroundColorCode: String
  var user: String
}

This might look redundant at first; technically, it is. However, remember our earlier note about how Stencil can only serve stored properties? Stencil cannot handle computed properties of some objects. Notice that this object conforms to Codable, too!

Now, you might be thinking: “Wait… my JournalEntry object doesn’t have any computed properties!” Take a deep breath; it doesn’t. However, you’re going to extend it here so that it does for the sake of convenience.

Scroll up to the top of this file, but just underneath your imports, and add the following three computed properties to a fileprivate extension of your object:

fileprivate extension UserJournalEntry {
  var displayDate: String {
    get {
      let formatter = DateFormatter()
      formatter.dateStyle = .long
      return formatter.string(from: self.date)
    }
  }
  
  var displayTime: String {
    get {
      let formatter = DateFormatter()
      formatter.timeStyle = .long
      return formatter.string(from: self.date)
    }
  }
  
  var backgroundColorCode: String {
    get {
      guard let substring = id?.suffix(6).uppercased() else {
        return "000000"
      }
      return substring
    }
  }
}

A couple of points about what you’ve just added:

  • You want to make sure you are extending UserJournalEntry, which will include the user in every object you pass through to your Stencil context.
  • displayDate and displayTime are purely for convenience. You could absolutely create these variables from the date property you pass to your HTML page, but this allows you to do it with Swift and make your HTML a bit simpler!
  • The backgroundColorCode is a design decision made in lockstep with your iOS app, based on the ID of a journal entry object. Hey, it works!

Alright, now you’ve got the object that you’re going to pass into the context.

Add the following code to your showClient route handler:

UserJournalEntry.findAll(using: Database.default) { 
  entries, error in
  
  guard let entries = entries else {
    response.status(.serviceUnavailable).send(json: ["Message": 
      "Service unavailable:" +
      "\(String(describing: error?.localizedDescription))"])
    return
  }
  
  let sortedEntries = entries.sorted(by: {
    $0.date.timeIntervalSince1970 > 
    $1.date.timeIntervalSince1970
  })
}

Notice that you are making use of the ORM function on UserJournalEntry instead of just JournalEntry. This is to override the user authentication on the server side — temporarily, of course! Later on you’ll need to handle authentication properly. By guarding against a potential issue with the database, you make sure that you protect your web client against unexpected issues.

After you get a handle on your array of entries, then you sort them so they are date-descending.

Next, you’re going to render the final array of objects that you’ll send to your .stencil file. Inside the closure for your UserJournalEntry.findAll function, but at the very bottom, add the following code:

//1
var webEntries = [JournalEntryWeb]()

for entry in sortedEntries {

  // 2
  webEntries.append(JournalEntryWeb(id: entry.id, 
    emoji: entry.emoji, date: entry.date, 
    displayDate: entry.displayDate, 
    displayTime: entry.displayTime, 
    backgroundColorCode: entry.backgroundColorCode, 
    user: entry.user))
}

// 3
do {
  try response.render("home.stencil", with: webEntries, 
    forKey: "entries")
} catch let error {
  response.status(.internalServerError)
    .send(error.localizedDescription)
}

With this code, you:

  1. Create a buffer array of JournalEntryWeb objects to send over.
  2. Populate it using a combination of the computed properties from your extension in this file and the stored properties this object already carries.
  3. Stuff your response.render command into a do-catch block, and you’re scot free!

Lastly, in Xcode, open Sources/Application/Application.swift and go to postInit. Right beneath where you call initializeUserRoutes, add the following function:

initializeWebClientRoutes(app: self)

Nice! Now everything is ready to go. Go back to WebClientRoutes.swift, set a breakpoint inside showClient.

Build and run your server in Xcode.

Open up a web browser, and visit http://localhost:8080/client. Your breakpoint should trigger; step through the inherent functionality and watch your context build! After you let your breakpoint go and let the route handler finish, check your browser and…

Initial web client

To test out your web UI, you should add a couple of journal entries if you don’t have any already. You can use the OpenAPI Spec with your Kitura app to do so, by visiting http://localhost:8080/openapi/ui. For instructions on using OpenAPI, see The OpenAPI Spec and Kitura: Getting Started. You’ll need to create a user and then use the /entries POST command to add an entry into your database.