Swift Report Builder Project
A report builder project is a small toolkit that assembles report lines from clamped scores supplied by an actor. Each step adds one part, and the finished program prints the same report on every run.
Clamped scores inside a built report
The Clamped wrapper pulls the score of Ana down from 120 to 100 as the entry is built. The builder gathers a title line, a branch line and one line per entry.
@propertyWrapper
struct Clamped {
private var stored: Int
var wrappedValue: Int {
get { stored }
set { stored = min(max(newValue, 0), 100) }
}
init(wrappedValue: Int) {
stored = min(max(wrappedValue, 0), 100)
}
}
@resultBuilder
struct ReportBuilder {
static func buildExpression(_ expression: String) -> [String] {
[expression]
}
static func buildBlock(_ components: [String]...) -> [String] {
components.flatMap { $0 }
}
static func buildOptional(_ component: [String]?) -> [String] {
component ?? []
}
static func buildEither(first component: [String]) -> [String] {
component
}
static func buildEither(second component: [String]) -> [String] {
component
}
static func buildArray(_ components: [[String]]) -> [String] {
components.flatMap { $0 }
}
}
func buildReport(@ReportBuilder _ content: () -> [String]) -> [String] {
content()
}
struct Entry {
var name: String
@Clamped var score: Int
}
let entries = [
Entry(name: "Ana", score: 120),
Entry(name: "Bo", score: 85)
]
let lines = buildReport {
"Class Report"
if entries.count > 1 {
"Full group"
} else {
"Small group"
}
for entry in entries {
"\(entry.name): \(entry.score)"
}
}
for line in lines {
print(line)
}A generic shelf that sorts its entries
The shelf stores entries in the order they were added, which puts Bo before Ana. The extension with the where clause returns the entries sorted by name for the report.
@propertyWrapper
struct Clamped {
private var stored: Int
var wrappedValue: Int {
get { stored }
set { stored = min(max(newValue, 0), 100) }
}
init(wrappedValue: Int) {
stored = min(max(wrappedValue, 0), 100)
}
}
@resultBuilder
struct ReportBuilder {
static func buildExpression(_ expression: String) -> [String] {
[expression]
}
static func buildBlock(_ components: [String]...) -> [String] {
components.flatMap { $0 }
}
static func buildOptional(_ component: [String]?) -> [String] {
component ?? []
}
static func buildEither(first component: [String]) -> [String] {
component
}
static func buildEither(second component: [String]) -> [String] {
component
}
static func buildArray(_ components: [[String]]) -> [String] {
components.flatMap { $0 }
}
}
func buildReport(@ReportBuilder _ content: () -> [String]) -> [String] {
content()
}
struct Entry {
var name: String
@Clamped var score: Int
}
struct Shelf<Item> {
var items: [Item] = []
mutating func add(_ item: Item) {
items.append(item)
}
}
extension Shelf where Item == Entry {
func sortedEntries() -> [Entry] {
items.sorted { $0.name < $1.name }
}
}
var shelf = Shelf<Entry>()
shelf.add(Entry(name: "Bo", score: 85))
shelf.add(Entry(name: "Ana", score: 120))
let entries = shelf.sortedEntries()
let lines = buildReport {
"Class Report"
if entries.count > 0 {
"\(entries.count) entries"
}
for entry in entries {
"\(entry.name): \(entry.score)"
}
}
for line in lines {
print(line)
}An actor supplies the scores
The actor stores the raw scores and hands back one snapshot when awaited. The main flow walks the sorted snapshot keys, so the entries always reach the shelf in name order.
@propertyWrapper
struct Clamped {
private var stored: Int
var wrappedValue: Int {
get { stored }
set { stored = min(max(newValue, 0), 100) }
}
init(wrappedValue: Int) {
stored = min(max(wrappedValue, 0), 100)
}
}
@resultBuilder
struct ReportBuilder {
static func buildExpression(_ expression: String) -> [String] {
[expression]
}
static func buildBlock(_ components: [String]...) -> [String] {
components.flatMap { $0 }
}
static func buildOptional(_ component: [String]?) -> [String] {
component ?? []
}
static func buildEither(first component: [String]) -> [String] {
component
}
static func buildEither(second component: [String]) -> [String] {
component
}
static func buildArray(_ components: [[String]]) -> [String] {
components.flatMap { $0 }
}
}
func buildReport(@ReportBuilder _ content: () -> [String]) -> [String] {
content()
}
struct Entry {
var name: String
@Clamped var score: Int
}
struct Shelf<Item> {
var items: [Item] = []
mutating func add(_ item: Item) {
items.append(item)
}
}
extension Shelf where Item == Entry {
func sortedEntries() -> [Entry] {
items.sorted { $0.name < $1.name }
}
}
actor ScoreStore {
private var scores: [String: Int] = [:]
func set(_ name: String, _ value: Int) {
scores[name] = value
}
func snapshot() -> [String: Int] {
scores
}
}
let store = ScoreStore()
await store.set("Bo", 85)
await store.set("Ana", 120)
await store.set("Cy", -5)
let snapshot = await store.snapshot()
var shelf = Shelf<Entry>()
for name in snapshot.keys.sorted() {
shelf.add(Entry(name: name, score: snapshot[name] ?? 0))
}
let entries = shelf.sortedEntries()
let lines = buildReport {
"Class Report"
if entries.count > 2 {
"Top group"
} else {
"Small group"
}
for entry in entries {
"\(entry.name): \(entry.score)"
}
}
for line in lines {
print(line)
}The branch line takes its other side
Three entries stay under the top group threshold, so the builder branch contributes the small group line. The sorted walk still fixes Ana, Mia and Zed as the entry order.
@propertyWrapper
struct Clamped {
private var stored: Int
var wrappedValue: Int {
get { stored }
set { stored = min(max(newValue, 0), 100) }
}
init(wrappedValue: Int) {
stored = min(max(wrappedValue, 0), 100)
}
}
@resultBuilder
struct ReportBuilder {
static func buildExpression(_ expression: String) -> [String] {
[expression]
}
static func buildBlock(_ components: [String]...) -> [String] {
components.flatMap { $0 }
}
static func buildOptional(_ component: [String]?) -> [String] {
component ?? []
}
static func buildEither(first component: [String]) -> [String] {
component
}
static func buildEither(second component: [String]) -> [String] {
component
}
static func buildArray(_ components: [[String]]) -> [String] {
components.flatMap { $0 }
}
}
func buildReport(@ReportBuilder _ content: () -> [String]) -> [String] {
content()
}
struct Entry {
var name: String
@Clamped var score: Int
}
struct Shelf<Item> {
var items: [Item] = []
mutating func add(_ item: Item) {
items.append(item)
}
}
extension Shelf where Item == Entry {
func sortedEntries() -> [Entry] {
items.sorted { $0.name < $1.name }
}
}
actor ScoreStore {
private var scores: [String: Int] = [:]
func set(_ name: String, _ value: Int) {
scores[name] = value
}
func snapshot() -> [String: Int] {
scores
}
}
let store = ScoreStore()
await store.set("Zed", 40)
await store.set("Mia", 64)
await store.set("Ana", 95)
let snapshot = await store.snapshot()
var shelf = Shelf<Entry>()
for name in snapshot.keys.sorted() {
shelf.add(Entry(name: name, score: snapshot[name] ?? 0))
}
let entries = shelf.sortedEntries()
let lines = buildReport {
"Class Report"
if entries.count > 5 {
"Top group"
} else {
"Small group"
}
for entry in entries {
"\(entry.name): \(entry.score)"
}
}
for line in lines {
print(line)
}Example
@propertyWrapper
struct Clamped {
private var stored: Int
var wrappedValue: Int {
get { stored }
set { stored = min(max(newValue, 0), 100) }
}
init(wrappedValue: Int) {
stored = min(max(wrappedValue, 0), 100)
}
}
@resultBuilder
struct ReportBuilder {
static func buildExpression(_ expression: String) -> [String] {
[expression]
}
static func buildBlock(_ components: [String]...) -> [String] {
components.flatMap { $0 }
}
static func buildOptional(_ component: [String]?) -> [String] {
component ?? []
}
static func buildEither(first component: [String]) -> [String] {
component
}
static func buildEither(second component: [String]) -> [String] {
component
}
static func buildArray(_ components: [[String]]) -> [String] {
components.flatMap { $0 }
}
}
func buildReport(@ReportBuilder _ content: () -> [String]) -> [String] {
content()
}
struct Entry {
var name: String
@Clamped var score: Int
}
struct Shelf<Item> {
var items: [Item] = []
mutating func add(_ item: Item) {
items.append(item)
}
}
extension Shelf where Item == Entry {
func sortedEntries() -> [Entry] {
items.sorted { $0.name < $1.name }
}
}
actor ScoreStore {
private var scores: [String: Int] = [:]
func set(_ name: String, _ value: Int) {
scores[name] = value
}
func snapshot() -> [String: Int] {
scores
}
}
let store = ScoreStore()
await store.set("Sam", 105)
await store.set("Kim", 77)
await store.set("Raj", 59)
await store.set("Ana", 92)
let snapshot = await store.snapshot()
var shelf = Shelf<Entry>()
for name in snapshot.keys.sorted() {
shelf.add(Entry(name: name, score: snapshot[name] ?? 0))
}
let entries = shelf.sortedEntries()
let lines = buildReport {
"Final Report"
if entries.count > 2 {
"Top group"
} else {
"Small group"
}
for entry in entries {
"\(entry.name): \(entry.score)"
}
"Entries: \(entries.count)"
}
for line in lines {
print(line)
}Key points
- A property wrapper clamps each score into the range from 0 to 100 when an entry is built.
- A result builder gathers report lines from plain lines, a condition and a loop.
- A generic shelf stores the entries, and an extension with a where clause sorts them by name.
- An actor holds the raw scores away from the main flow of the program.
- The main flow awaits a snapshot of the actor and walks its keys in sorted order.
- Sorted names fix the entry order before any report line is built.
In this example, the actor stores four raw scores and the main flow awaits one snapshot of them. Walking the sorted snapshot keys builds entries for Ana, Kim, Raj and Sam in that fixed order. The Clamped wrapper pulls the score of Sam down from 105 to 100 as his entry is built. The shelf returns the entries sorted by name through its where clause extension. The builder then gathers the title, the top group branch line, one line per entry and the closing count line.
Note
Every part is plain library code and the whole program stays in one file.
Try it Yourself
Edit the code, then press Run.
Running is simulated
Exercise
In the finished report program, why does the main flow walk the snapshot keys in sorted order?