Skip to content

Latest commit

 

History

History
537 lines (385 loc) · 13.6 KB

Tutorial2.md

File metadata and controls

537 lines (385 loc) · 13.6 KB

Tutorial 2

Building a receipt layout with Blueprint

This tutorial will build a receipt for a quick service restaurant.

Tutorial setup instructions

Goals

  • Show receipt information in a complex layout
  • Use a few of the built-in element types from Blueprint

Screenshot of finished tutorial


Picking up from Tutorial 1

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.

Making it scroll

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)

Scrolling label

Insetting content from the edge of the screen

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.

Scrolling inset label

Receipt data

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

    // ...

Show the total on the receipt

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!

Receipt with total

Show items on the receipt

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.

Receipt with total, subtotal, and tax

Showing items

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
        }
    }

}

Adding a horizontal rule

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 from BlueprintUICommonControls to avoid re-implementing this.

Styling

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.

Tutorial 2 complete

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.