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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Kitura Stencil Tutorial: How to make Websites with Swift
25 mins
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.
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.
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:
- 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!
-
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. -
Here, you are registering the
/client
route on your router, and you’ll handle this route with Stencil and Kitura shortly. - 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
anddisplayTime
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:
-
Create a buffer array of
JournalEntryWeb
objects to send over. - Populate it using a combination of the computed properties from your extension in this file and the stored properties this object already carries.
-
Stuff your
response.render
command into ado-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…
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.