Now that there is a FoodServer backend for the FoodTracker app, you can add a website front end so that the page displayed by your server is more visually appealing than returning a JSON array of meals. The steps below demonstrate how to add a Stencil template to your server so that it can return dynamic web pages, using Kitura-TemplateEngine and Kitura-StencilTemplateEngine.
These instructions follow on from the FoodTracker application and server created by following the FoodTrackerBackend tutorial. If you have completed the FoodTracker Backend there are no further pre-requisites.
If you have not completed the FoodTrackerBackend tutorial go to the CompletedFoodTracker branch and follow the README instructions.
The FoodTracker application is taken from the Apple tutorial for building your first iOS application. In the FoodTrackerBackend tutorial, we created a server and connected it to the iOS application. This means that when meals are added they are posted to the server, a user can then view these meals on localhost:8080/meals. When you perform a HTTP GET
request to the server, you are returned a JSON array which contains your stored meals. To present this data in a more appealing way we will use Stencil. Stencil is a template language for Swift which allows you to embed code into a HTML document to create a dynamic web page.
To use templates with a Kitura router, you must create a .stencil
template file which describes how to embed code into the returned HTML. By default, the Kitura router gets the template files from the ./Views/
directory in the directory where Kitura runs.
- In the terminal, change into the FoodServer directory:
cd ~/FoodTrackerBackend/FoodServer
- Create the Views folder and change into it:
mkdir Views
cd Views
- Create a
FoodTemplate.stencil
file, then open it with any text editor:
touch FoodTemplate.stencil
open -a Xcode.app FoodTemplate.stencil
NB: You can use any text editor to open the .stencil
file.
- Add the following Stencil template code to display the meals:
<html>
There are {{ meals.count }} meals. <br />
{% for meal in meals %}
- {{ meal.name }} with rating {{ meal.rating }} <br />
{% endfor %}
</html>
- Save the file and exit your text editor.
When the above code is rendered, it will display the total number of meals in your meal store, it will then loop through the meal store displaying each meal name and rating. For more details about Stencil templates see the Stencil User Guide.
Kitura-StencilTemplateEngine, allows users to easily use Stencil templates in Swift and needs to be added to our Package.swift
file.
- In the terminal, go to your server's
Package.swift
file.
cd ~/FoodTrackerBackend/FoodServer
open Package.swift
- Add the
Kitura-StencilTemplateEngine
package:
.package(url: "https://github.com/IBM-Swift/Kitura-StencilTemplateEngine.git", from: "1.8.0")
- Change the target for Application to include "KituraStencil":
.target(name: "Application", dependencies: [ "Kitura", "CloudEnvironment", "SwiftMetrics", "Health", "SwiftKueryORM", "SwiftKueryPostgreSQL", "KituraStencil"]),
- Regenerate your FoodServer Xcode project:
swift package generate-xcodeproj
- Open your
Sources > Application > Application.swift
file - Add KituraStencil to the import statements:
import KituraStencil
- Add the template engine to the router.
Inside the postInit()
function, insert the following line, below router.get("/meals", handler: loadHandler)
:
router.add(templateEngine: StencilTemplateEngine())
The router will now use a StencilTemplateEngine()
to render .stencil
templates.
Here we define a new route for our server which we will use to return the formatted HTML. This is done using Kitura 1 style (i.e. not Codable) routing with request
, response
and next
parameters. We will render the .stencil
file and add this to the response
.
- Add a new route for
GET
on "/foodtracker". Add the following code on the line belowrouter.add(templateEngine: StencilTemplateEngine())
:
router.get("/foodtracker") { request, response, next in
next()
}
- Build a JSON string description of the FoodTracker meal store.
Add the following code inside your “/foodtracker" route above
next()
:
Meal.findAll { (result: [Meal]?, error: RequestError?) in
guard let meals = result else {
return
}
var allMeals: [String: [[String:Any]]] = ["meals" :[]]
for meal in meals {
allMeals["meals"]?.append(["name": meal.name, "rating": meal.rating])
}
}
- Render the template and add it to your
response
. Add the following line abovenext()
:
do {
try response.render("FoodTemplate.stencil", context: allMeals)
} catch let error {
response.send(json: ["Error": error.localizedDescription])
}
This will render the FoodTemplate.stencil
file using allMeals
to embed variables from the code.
- Your completed "/foodtracker" route should now look as follows:
router.get("/foodtracker") { request, response, next in
Meal.findAll { (result: [Meal]?, error: RequestError?) in
guard let meals = result else {
return
}
var allMeals: [String: [[String:Any]]] = ["meals" :[]]
for meal in meals {
allMeals["meals"]?.append(["name": meal.name, "rating": meal.rating])
}
do {
try response.render("FoodTemplate.stencil", context: allMeals)
} catch let error {
response.send(json: ["Error": error.localizedDescription])
}
next()
}
}
We can test this route by running the FoodTracker application and the FoodServer. Add a meal in the app and then go to http://localhost:8080/foodtracker. This will now display a line saying how many meals are present in the app and a list of the meal names and ratings.
The next sections will take you through saving and displaying the meal photograph, receiving new meals from a web form and adding CSS to the webpage.
Our meal tracker application allows users to upload a photograph of their meal. We would like to add this photograph to our web page as a picture and not as a string of data as it is currently displayed. To achieve this we will save the user photos and then implement a Static File Server which will serve the photos using our Stencil template. Note If you have completed "AddWebApplication.md", you will already have the required Static File Server.
- In the terminal, create the
public
directory:
cd ~/FoodTrackerBackend/FoodServer
mkdir public
The default location for a static file server to serve files from is the ./public
directory so we will create this directory on our server to save our users' pictures in.
- Open your
Sources
>Application
>Application.swift
file. - Set up the file handler to write to the web hosting directory by adding the following under the
cloudEnv
declaration:
private var fileManager = FileManager.default
private var rootPath = StaticFileServer().absoluteRootPath
- Save the pictures received by the server.
Add the following code to the beginning of your
storeHandler
:
let path = "\(self.rootPath)/\(meal.name).jpg"
fileManager.createFile(atPath: path, contents: meal.photo)
This will create a file with the name of your meal and a .jpg extension inside the public directory of your server. If the file already exists it will overwrite it with a new picture. You can test this by re-running the FoodServer with your changes and adding a meal - the photo should appear in the public
directory you just created.
- Your
storeHandler
function should now look as follows:
func storeHandler(meal: Meal, completion: (Meal?, RequestError?) -> Void ) {
let path = "\(self.rootPath)/\(meal.name).jpg"
fileManager.createFile(atPath: path, contents: meal.photo)
meal.save(completion)
}
- Add a static file server route for images.
Insert the following line below router.get("/meals", handler: loadHandler)
:
router.get("/images", middleware: StaticFileServer())
- Open your "FoodTemplate.stencil" file:
cd ~/FoodTrackerBackend/FoodServer/Views
open -a Xcode.app FoodTemplate.stencil
- Add the following line in your
for
loop below the line- {{ meal.name }} with rating {{ meal.rating }}. <br />
<img src="images/{{ meal.name }}.jpg" alt="meal image" height="100"> <br />
This will make a call to the server for the image saved with the meal and display it.
Restart your server to add your new changes. Then add a new meal and view your front end meal store at http://localhost:8080/foodtracker. You should see a webpage displaying the total number of meals and a list of the meal names, rating and as well as a picture of the meal. Congratulations! You have now taken a meal from the app, set up a Kitura server to receive the data and displayed it embedded in HTML to a web page.
The user wants to be able to submit meals from the web page as well as the app. We will use a HTML form to post data to the Kitura server. We will parse the received data using the Kitura BodyParser
, add a meal to our meal store and display the new meal.
- In the terminal, change into the
Views
directory:
cd ~/FoodTrackerBackend/FoodServer/Views
- Open your
FoodTemplate.stencil
file:
open -a Xcode.app FoodTemplate.stencil
- Add the following code below
{% endfor %}
:
<form action="foodtracker" method="post" enctype="multipart/form-data">
Name: <input type="text" name="name"><br>
Rating: <input type="range" name="rating" min="0" max="5"><br>
File: <input type="file" name="photo"><br>
<input type="submit" value="Submit">
</form>
- Refresh your http://localhost:8080/foodtracker to view the form.
This code creates a multipart form with three fields. A name text box, a rating slider and a Browse
button to select files. When you click submit, it will POST
a multipart form to "/foodtracker" with the data from the form.
- Open the FoodServer
Sources
>Application
>Application.swift
file. - Connect the
BodyParser
middleware.
Inside the postInit()
function, add BodyParser
to your route:
router.post("/foodtracker", middleware: BodyParser())
- Add a Kitura 1 style
POST
route for "/foodtracker".
Add the following code beneath your BodyParser
route:
router.post("/foodtracker") { request, response, next in
next()
}
- Parse the
request
body withBodyParser
.
Add the following code inside your “/foodtracker" post route above next()
:
guard let parsedBody = request.body else {
next()
return
}
- Split the
parsedBody
into a MultiPart array.
Add the following line after the guard
statement:
let parts = parsedBody.asMultiPart
- Parse the MultiPart array as a meal object.
Insert the following code beneath your parts
declaration:
guard let name = parts?[0].body.asText,
let stringRating = parts?[1].body.asText,
let rating = Int(stringRating),
case .raw(let photo)? = parts?[2].body,
parts?[2].type == "image/jpeg",
let newMeal = Meal(name: name, photo: photo, rating: rating)
else {
next()
return
}
- Save the meal and the photo.
Insert the following code after the else block:
let path = "\(self.rootPath)/\(newMeal.name).jpg"
self.fileManager.createFile(atPath: path, contents: newMeal.photo)
newMeal.save { (meal: Meal?, error: RequestError?) in
next()
}
For simplicity we are only accepting .jpg
files from the web page.
- Redirect the user to the
GET
"/foodtracker" route.
Add the following line below the route declaration:
try response.redirect("/foodtracker")
This will reload the page and prevent meals being submitted multiple times.
- Your completed "/foodtracker"
POST
route should now look as follows:
router.post("/foodtracker") { request, response, next in
try response.redirect("/foodtracker")
guard let parsedBody = request.body else {
next()
return
}
let parts = parsedBody.asMultiPart
guard let name = parts?[0].body.asText,
let stringRating = parts?[1].body.asText,
let rating = Int(stringRating),
case .raw(let photo)? = parts?[2].body,
parts?[2].type == "image/jpeg",
let newMeal = Meal(name: name, photo: photo, rating: rating)
else {
next()
return
}
let path = "\(self.rootPath)/\(newMeal.name).jpg"
self.fileManager.createFile(atPath: path, contents: newMeal.photo)
newMeal.save { (meal: Meal?, error: RequestError?) in
next()
}
}
Restart your server to add your new changes. When you add a new meal at http://localhost:8080/foodtracker, you should see the webpage update with your new meal. Since the requests are asyc, you may need to refresh the webpage to see the new meal.
We have provided some basic html and css in a file called Example.stencil
. To improve the presentation of the webpage, we will serve this template instead of FoodTemplate.stencil
.
- Open your terminal window.
- Change directory to your FoodTrackerBackend
cd ~/FoodTrackerBackend/
- Move
Example.Stencil
to your Views folder.
mv Example.stencil FoodServer/Views/
-
Open your
Sources
>Application
>Application.swift
file. -
Replace
FoodTemplate.stencil
withExample.stencil
in theresponse.render
call:
try response.render("Example.stencil", context: allMeals)
- Restart the server to compile the new route.
Now view your webpage at http://localhost:8080/foodtracker. Your front end will have CSS styling to create a complete food tracker website!
Any questions or comments? Please join the Kitura community on Slack!