This tutorial will build a receipt for a quick service restaurant.
- Show receipt information in a complex layout
- Use a few of the built-in element types from Blueprint
In Tutorial 1 we added Blueprint to a view controller, and created a new HelloWorldElement
to display in it.
struct HelloWorldElement: ProxyElement {
var elementRepresentation: Element {
Label(text: "Hello, world") { label in
label.font = .boldSystemFont(ofSize: 24.0)
label.color = .darkGray
}
.centered()
}
}
We'll be building a receipt layout in this tutorial. You can repurpose HelloWorldElement
for this: just rename it to ReceiptElement
and we'll continue from there.
Receipts can get pretty long, so we'll give this receipt the ability to scroll right off the bat.
Instead of returning Centered(label)
, we'll make a ScrollView
to display the label:
var scrollView = ScrollView(wrapping: label)
Next, we'll configure the `ScrollView and return it.
struct ReceiptElement: ProxyElement {
var elementRepresentation: Element {
Label(text: "Hello, world") { label in
label.font = .boldSystemFont(ofSize: 24.0)
label.color = .darkGray
}
.scrollable { scrollView in
scrollView.alwaysBounceVertical = true
}
}
}
If you run Tutorial 2
in the simulator, you should be able to scroll vertically (though with a single label there is nothing to scroll just yet)
So we now have a scrolling label, but it's jammed right up against the edge of the screen. We'll inset everything inside the ScrollView
to make sure we have consistent padding.
We do this with the Inset
element. We'll wrap the label in an Inset
before we place it into the ScrollView
.
struct ReceiptElement: ProxyElement {
var elementRepresentation: Element {
Label(text: "Hello, world") { label in
label.font = .boldSystemFont(ofSize: 24.0)
label.color = .darkGray
}
.inset(uniform: 24.0)
.scrollable { scrollView in
scrollView.alwaysBounceVertical = true
}
}
}
If we run Tutorial 2
again, we can see that the label is now inset from the edge of the screen.
We're about ready to start building the contents of the receipt, so we'll go ahead and give ourselves some data.
We've defined a Purchase
model below (complete with sample data). Don't feel compelled to type this, you can copy and paste this one.
Create a new file called Purchase.swift
(make sure it's part of Tutorial 2
) and copy the following code into it:
struct Purchase {
var items: [Item]
var subtotal: Double {
items
.map { $0.price }
.reduce(0.0, +)
}
var tax: Double {
subtotal * 0.085
}
var total: Double {
subtotal + tax
}
struct Item {
var name: String
var price: Double
}
static var sample: Purchase {
Purchase(items: [
Item(name: "Burger", price: 7.99),
Item(name: "Fries", price: 2.49),
Item(name: "Soda", price: 1.49)
])
}
}
Next, add a property to ReceiptElement
to hold a purchase to be displayed:
struct ReceiptElement: ProxyElement {
let purchase = Purchase.sample
// ...
Receipts are typically arranged in a vertical stack of line items. We'll start simple and build a single row first, in which we will display the purchase total.
We'll do this with a new element:
struct LineItemElement: ProxyElement {
var elementRepresentation: Element {
// TODO
}
}
Line items on a receipt show text on one side, and a price on the other side. Let's add a couple of properties to LineItemElement
to make sure we have the values that we will be displaying.
struct LineItemElement: ProxyElement {
var title: String
var price: Double
var elementRepresentation: Element {
// TODO
}
}
Now that we have content to show, we need to configure the elements that will actually show that content. We know that we want the title to appear on the left and the price to appear on the right. Both pieces of information should be aligned vertically.
This is a great time to use one of the most common Blueprint elements: Row
.
Row
arranges (stacks) its children along the horizontal axis.
struct LineItemElement: ProxyElement {
var title: String
var price: Double
var elementRepresentation: Element {
Row(underflow: .spaceEvenly) {
Label(text: title)
Label(text: formattedPrice)
}
}
private var formattedPrice: String {
let formatter = NumberFormatter()
formatter.numberStyle = .currency
return formatter.string(from: NSNumber(value: price)) ?? ""
}
}
Notice how we create a row: its initializer takes a single closure parameter, which is used to configure the row. This is useful because rows can have multiple children, so it would be very awkward to try to pass everything in to the initializer at once.
Let's go back to ReceiptElement
.
We'll now replace the "Hello, World" label with a LineItemElement
:
struct ReceiptElement: ProxyElement {
var elementRepresentation: Element {
LineItemElement(
title: "Total",
price: purchase.total
)
.scrollable { scrollView in
scrollView.alwaysBounceVertical = true
}
}
}
Run Tutorial 2
, and you should see the receipt total!
We can now show a single line item at a time. To show all of the items on the receipt, however, we'll need to vertically stack multiple line items.
While the Row
element that we used to implement LineItemElement
stacks elements horizontally, Column
is an element that stacks them vertically.
We will use a Column
to contain all of the line items that we ultimately want to show.
Update ReceiptElement
to wrap the line item in a Column
:
struct ReceiptElement: ProxyElement {
let purchase = Purchase.sample
var elementRepresentation: Element {
Column(alignment: .fill, minimumSpacing: 16) {
LineItemElement(
title: "Total",
price: purchase.total
)
}
.inset(uniform: 24)
.scrollable { scrollView in
scrollView.alwaysBounceVertical = true
}
}
}
If you were to run the app at this point, it wouldn't look any different from last time. We're now ready to add more line items, however. Let's add subtotal and tax:
struct ReceiptElement: ProxyElement {
let purchase = Purchase.sample
var elementRepresentation: Element {
Column(alignment: .fill, minimumSpacing: 16) {
LineItemElement(
title: "Subtotal",
price: purchase.subtotal
)
LineItemElement(
title: "Tax",
price: purchase.tax
)
LineItemElement(
title: "Total",
price: purchase.total
)
}
.inset(uniform: 24)
.scrollable { scrollView in
scrollView.alwaysBounceVertical = true
}
}
}
Run Tutorial 2
, and you should see three separate line items, stacked vertically.
We now have everything that we need to show all of the items from the purchase as well.
In ReceiptElement
, we'll iterate over all of the items in purchase.item
and add a line item for each:
struct ReceiptElement: ProxyElement {
let purchase = Purchase.sample
var elementRepresentation: Element {
Column(alignment: .fill, minimumSpacing: 16) {
for item in purchase.items {
LineItemElement(
title: item.name,
price: item.price
)
}
RuleElement()
LineItemElement(
title: "Subtotal",
price: purchase.subtotal
)
LineItemElement(
title: "Tax",
price: purchase.tax
)
LineItemElement(
title: "Total",
price: purchase.total
)
}
.inset(uniform: 24)
.scrollable { scrollView in
scrollView.alwaysBounceVertical = true
}
}
}
If you were to run the app right now, you might notice that it's hard to tell where the items stop and the subtotal/tax/total start. We can improve legibility by adding a horizontal rule.
We'll define RuleElement
like this:
import BlueprintUI
import BlueprintUICommonControls
struct RuleElement: ProxyElement {
var elementRepresentation: Element {
Box(backgroundColor: .black)
.constrainedTo(height: .absolute(1.0))
}
}
We use a Box
so that we can specify a background color.
We then wrap the box in a ConstrainedSize
. We leave the width untouched, but we constrain the height to always require exactly 1 point.
We can then add the rule in between the items and the extra info inside ReceiptElement
:
// ...
for item in purchase.items {
LineItemElement(
title: item.name,
price: item.price
)
}
RuleElement()
LineItemElement(
title: "Subtotal",
price: purchase.subtotal
)
// ...
Note: in a real app, you can drop in the
Rule
element fromBlueprintUICommonControls
to avoid re-implementing this.
Let's add some text styles to introduce visual contrast to our line items.
We want the total line to appear heavier than the rest of the items, so we can add an enum
to model the different styles that LineItemElement
should support:
extension LineItemElement {
enum Style {
case regular
case bold
}
}
We'll extend that Style
enum to provide fonts and colors for the title and price labels:
extension LineItemElement {
enum Style {
case regular
case bold
fileprivate var titleFont: UIFont {
switch self {
case .regular: return .systemFont(ofSize: 18.0)
case .bold: return .boldSystemFont(ofSize: 18.0)
}
}
fileprivate var titleColor: UIColor {
switch self {
case .regular: return .gray
case .bold: return .black
}
}
fileprivate var priceFont: UIFont {
switch self {
case .regular: return .systemFont(ofSize: 18.0)
case .bold: return .boldSystemFont(ofSize: 18.0)
}
}
fileprivate var priceColor: UIColor {
switch self {
case .regular: return .black
case .bold: return .black
}
}
}
}
Add a style
property to LineItemElement
, then update its implementation to use the style:
struct LineItemElement: ProxyElement {
var style: Style
var title: String
var price: Double
var elementRepresentation: Element {
Row(underflow: .spaceEvenly) {
Label(text: title) { label in
label.font = style.titleFont
label.color = style.titleColor
}
Label(text: formattedPrice) { label in
label.font = style.priceFont
label.color = style.priceColor
}
}
}
private var formattedPrice: String {
let formatter = NumberFormatter()
formatter.numberStyle = .currency
return formatter.string(from: NSNumber(value: price)) ?? ""
}
}
Finally, update ReceiptElement
to pass in the correct style for each line item: .regular
for everything except the total, whould should receive .bold
.
struct ReceiptElement: ProxyElement {
let purchase = Purchase.sample
var elementRepresentation: Element {
Column(alignment: .fill, minimumSpacing: 16) {
for item in purchase.items {
LineItemElement(
style: .regular,
title: item.name,
price: item.price
)
}
RuleElement()
LineItemElement(
style: .regular,
title: "Subtotal",
price: purchase.subtotal
)
LineItemElement(
style: .regular,
title: "Tax",
price: purchase.tax
)
LineItemElement(
style: .bold,
title: "Total",
price: purchase.total
)
}
.inset(uniform: 24)
.scrollable { scrollView in
scrollView.alwaysBounceVertical = true
}
}
}
Run Tutorial 2
in the simulator.
The receipt is now complete! Experiment with adding more items, modifying the scroll view, or changing any of the other elements that we created along the way.